{
 ***************************************************************************
 *                                                                         *
 *   This source is free software; you can redistribute it and/or modify   *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This code is distributed in the hope that it will be useful, but      *
 *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
 *   General Public License for more details.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 *                                                                         *
 ***************************************************************************

  Author: Mattias Gaertner

  Abstract:
    Frame for messages - output lines for (compiler) messages.
}
unit etMessageFrame;

{$mode objfpc}{$H+}

{$I ide.inc}

interface

uses
  Math, strutils, Classes, SysUtils,
  UTF8Process, FileProcs, LazFileCache,
  LazUTF8Classes, LazFileUtils, LazUTF8, AvgLvlTree, SynEdit,
  LResources, Forms, Buttons, ExtCtrls, Controls, LMessages,
  LCLType, Graphics, LCLIntf, Themes, ImgList, GraphType, Menus, Clipbrd,
  Dialogs, StdCtrls,
  SynEditMarks,
  // IDEIntf
  IDEExternToolIntf, IDEImagesIntf, MenuIntf, PackageIntf,
  IDECommands, IDEDialogs, ProjectIntf, CompOptsIntf, LazIDEIntf,
  // IDE
  LazarusIDEStrConsts, EnvironmentOpts, HelpFPCMessages, etSrcEditMarks,
  MsgWnd_Options, etQuickFixes, ExtTools, IDEOptionDefs, CompilerOptions;

const
  CustomViewCaption = '------------------------------';

type
  TMessagesCtrl = class;

  { TLMsgWndView }

  TLMsgWndView = class(TLazExtToolView)
  private
    FControl: TMessagesCtrl;
    FFilter: TLMsgViewFilter;
    fPaintBottom: integer; // only valid if FPaintStamp=Control.FPaintStamp
    FPaintStamp: int64;
    fPaintTop: integer; // only valid if FPaintStamp=Control.FPaintStamp
    FPendingChanges: TETMultiSrcChanges;
    procedure SetFilter(AValue: TLMsgViewFilter);
    procedure OnMarksFixed(ListOfTMessageLine: TFPList); // (main thread) called after mlfFixed was added to these messages
  protected
    procedure SetToolState(AValue: TLMVToolState); override;
    procedure FetchAllPending; override; // (main thread)
    procedure ToolExited; override; // (main thread)
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function LineFits(Line: TMessageLine): boolean; override; // (worker thread)
    property Control: TMessagesCtrl read FControl;
    function HasContent: boolean;
    function GetShownLineCount(WithHeader, WithProgressLine: boolean): integer;
    procedure RebuildLines; // (main thread)
    function ApplySrcChanges(Changes: TETSingleSrcChanges): boolean; // true if something changed
  public
    // requires Enter/LeaveCriticalSection, write only via main thread
    property Filter: TLMsgViewFilter read FFilter write SetFilter;
    property PendingChanges: TETMultiSrcChanges read FPendingChanges;// src changes for messages adding to view
  end;

  { TMsgCtrlUrgencyStyle }

  TMsgCtrlUrgencyStyle = class
  private
    FColor: TColor;
    FControl: TMessagesCtrl;
    FImageIndex: integer;
    FTranslated: string;
    FUrgency: TMessageLineUrgency;
    procedure SetColor(AValue: TColor);
    procedure SetImageIndex(AValue: integer);
    procedure SetTranslated(AValue: string);
    procedure Changed;
  public
    constructor Create(AControl: TMessagesCtrl; TheUrgency: TMessageLineUrgency);
    function Equals(Obj: TObject): boolean; override;
    procedure Assign(Src: TMsgCtrlUrgencyStyle);
    procedure SetValues(TheTranslated: string; TheImageIndex: integer = -1; TheColor: TColor = clDefault);
    property Control: TMessagesCtrl read FControl;
    property Urgency: TMessageLineUrgency read FUrgency;
    property Translated: string read FTranslated write SetTranslated;
    property ImageIndex: integer read FImageIndex write SetImageIndex default -1;
    property Color: TColor read FColor write SetColor default clDefault;
  end;

type
  TOnOpenMessageLine = function(Sender: TObject; Msg: TMessageLine): boolean of object;

  TMsgCtrlState = (
    mcsFocused
    );
  TMsgCtrlStates = set of TMsgCtrlState;

  TMsgCtrlOption = (
    mcoSingleClickOpensFile, // otherwise double click
    mcoShowStats, // show numbers of errors, warnings and hints in view header line
    mcoShowTranslated, // show translation (e.g. messages from German message file)
    mcoShowMessageID,  // show message ID
    mcoShowMsgIcons,
    mcoAutoOpenFirstError, // when all views stopped, open first error
    mcoSrcEditPopupSelect, // when user right clicks on gutter mark,
                           // scroll and select message of the quickfixes
    mcoWndStayOnTop,       // use fsStayOnTop
    mcoAlwaysDrawFocused   // draw selected item as focused, even if the window is not
    );
  TMsgCtrlOptions = set of TMsgCtrlOption;
const
  MCDefaultOptions = [mcoShowStats,mcoShowTranslated,
                      mcoAutoOpenFirstError,mcoShowMsgIcons,
                      mcoSrcEditPopupSelect];

type

  { TMessagesCtrl }

  TMessagesCtrl = class(TCustomControl)
  private
    FActiveFilter: TLMsgViewFilter;
    FBackgroundColor: TColor;
    FFilenameStyle: TMsgWndFileNameStyle;
    FHeaderBackground: array[TLMVToolState] of TColor;
    FIdleConnected: boolean;
    FImageChangeLink: TChangeLink;
    FImages: TCustomImageList;
    FItemHeight: integer;
    FOnAllViewsStopped: TNotifyEvent;
    FOnOpenMessage: TOnOpenMessageLine;
    FOnOptionsChanged: TNotifyEvent;
    FOptions: TMsgCtrlOptions;
    FScrollLeft: integer;
    FScrollTop: integer;
    fScrollTopMax: integer;
    FSearchText: string;
    FSelectedLine: integer;
    FSelectedView: TLMsgWndView;
    FSourceMarks: TETMarks;
    FTextColor: TColor;
    fUpdateLock: integer;
    FUpdateTimer: TTimer;
    fSomeViewsRunning: boolean;
    fUrgencyStyles: array[TMessageLineUrgency] of TMsgCtrlUrgencyStyle;
    FAutoHeaderBackground: TColor;
    procedure CreateSourceMark(MsgLine: TMessageLine; aSynEdit: TSynEdit);
    procedure CreateSourceMarks(View: TLMsgWndView; StartLineNumber: Integer);
    function GetActiveFilter: TLMsgViewFilter; inline;
    function GetHeaderBackground(aToolState: TLMVToolState): TColor;
    function GetSelectedLine: integer;
    function GetUrgencyStyles(Urgency: TMessageLineUrgency
      ): TMsgCtrlUrgencyStyle;
    function GetViews(Index: integer): TLMsgWndView;
    procedure OnViewChanged(Sender: TObject); // (main thread)
    procedure MsgUpdateTimerTimer(Sender: TObject);
    procedure SetActiveFilter(AValue: TLMsgViewFilter); inline;
    procedure SetBackgroundColor(AValue: TColor);
    procedure SetFilenameStyle(AValue: TMsgWndFileNameStyle);
    procedure SetHeaderBackground(aToolState: TLMVToolState; AValue: TColor);
    procedure SetIdleConnected(AValue: boolean);
    procedure SetImages(AValue: TCustomImageList);
    procedure SetItemHeight(AValue: integer);
    procedure SetOptions(NewOptions: TMsgCtrlOptions);
    procedure SetScrollLeft(AValue: integer);
    procedure SetScrollTop(AValue: integer);
    procedure SetSearchText(AValue: string);
    procedure SetSelectedLine(AValue: integer);
    procedure SetSelectedView(AValue: TLMsgWndView);
    procedure SetSourceMarks(AValue: TETMarks);
    procedure SetTextColor(AValue: TColor);
    procedure SetUrgencyStyles(Urgency: TMessageLineUrgency;
      AValue: TMsgCtrlUrgencyStyle);
    procedure SetAutoHeaderBackground(AValue: TColor);
    procedure WMHScroll(var Msg: TLMScroll); message LM_HSCROLL;
    procedure WMVScroll(var Msg: TLMScroll); message LM_VSCROLL;
    procedure WMMouseWheel(var Message: TLMMouseEvent); message LM_MOUSEWHEEL;
    procedure WMSetFocus(var Message: TLMSetFocus); message LM_SETFOCUS;
    procedure WMKillFocus(var Message: TLMKillFocus); message LM_KILLFOCUS;
    function GetMaxSelectedLine: integer;
    procedure ImageListChange(Sender: TObject);
    procedure OnIdle(Sender: TObject; var {%H-}Done: Boolean);
    procedure OnFilterChanged(Sender: TObject);
  protected
    FViews: TFPList;// list of TMessagesViewMap
    FStates: TMsgCtrlStates;
    FPaintStamp: int64;
    fLastSearchStartView: TLMsgWndView;
    fLastSearchStartLine: integer;
    fLastLoSearchText: string; // lower case search text
    procedure FetchNewMessages;
    function FetchNewMessages(View: TLMsgWndView): boolean; // true if new lines
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
    procedure Paint; override;
    procedure UpdateScrollBar(InvalidateScrollMax: boolean);
    procedure CreateWnd; override;
    procedure DoSetBounds(ALeft, ATop, AWidth, AHeight: integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer
      ); override;
    procedure KeyDown(var Key: Word; Shift: TShiftState); override;
    procedure DoOnShowHint(HintInfo: PHintInfo); override;
    procedure DoAllViewsStopped;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure BeginUpdate;
    procedure EndUpdate;
    procedure EraseBackground({%H-}DC: HDC); override;
    procedure ApplyEnvironmentOptions;
    function UrgencyToStr(Urgency: TMessageLineUrgency): string;

    // views
    function ViewCount: integer; inline;
    property Views[Index: integer]: TLMsgWndView read GetViews;
    function IndexOfView(View: TLMsgWndView): integer;
    procedure ClearViews(OnlyFinished: boolean); // deletes/frees all views
    procedure RemoveView(View: TLMsgWndView); // remove without free
    function GetView(aCaption: string; CreateIfNotExist: boolean): TLMsgWndView;
    function GetLineAt(Y: integer; out View: TLMsgWndView; out Line: integer): boolean;
    function GetLineText(Line: TMessageLine): string;
    function GetHeaderText(View: TLMsgWndView): string;
    function FindUnfinishedView: TLMsgWndView; // running or waiting for run
    function GetLastViewWithContent: TLMsgWndView;

    // filter
    property ActiveFilter: TLMsgViewFilter read GetActiveFilter write SetActiveFilter;
    function Filters: TLMsgViewFilters; inline;

    // select, search
    // Note: At the moment only single single selected is implemented
    function HasSelection: boolean;
    function IsLineSelected(View: TLMsgWndView; LineNumber: integer): boolean;
    procedure Select(View: TLMsgWndView; LineNumber: integer; DoScroll, FullyVisible: boolean);
    function SearchNext(StartView: TLMsgWndView; StartLine: integer;
      SkipStart, Downwards: boolean;
      out View: TLMsgWndView; out LineNumber: integer): boolean;
    procedure Select(Msg: TMessageLine; DoScroll: boolean);
    function SelectNextOccurence(Downwards: boolean): boolean;
    function SelectNextShown(Offset: integer): boolean;
    function SelectLast(DoScroll, FullyVisible: boolean): boolean;
    function SelectFirst(DoScroll, FullyVisible: boolean): boolean;
    function GetSelectedMsg: TMessageLine;
    function SearchNextUrgent(StartView: TLMsgWndView; StartLine: integer;
      SkipStart, Downwards: boolean;
      aMinUrgency: TMessageLineUrgency; WithSrcPos: boolean;
      out View: TLMsgWndView; out LineNumber: integer): boolean;
    function SelectFirstUrgentMessage(aMinUrgency: TMessageLineUrgency;
      WithSrcPos: boolean): boolean;
    function SelectNextUrgentMessage(aMinUrgency: TMessageLineUrgency;
      WithSrcPos: boolean; Downwards: boolean): boolean;

    // scroll
    function IsLineVisible(View: TLMsgWndView; LineNumber: integer): boolean;
    function IsLastLineVisible(View: TLMsgWndView): boolean;
    procedure ScrollToLine(View: TLMsgWndView; LineNumber: integer; FullyVisible: boolean);
    function GetLineTop(View: TLMsgWndView; LineNumber: integer; Scrolled: boolean): integer;
    property ScrollLeft: integer read FScrollLeft write SetScrollLeft;
    property ScrollTop: integer read FScrollTop write SetScrollTop;
    function ScrollLeftMax: integer;
    function ScrollTopMax: integer;
    procedure StoreSelectedAsSearchStart;

    // file
    function OpenSelection: boolean;
    procedure CreateMarksForFile(aSynEdit: TSynEdit; aFilename: string; DeleteOld: boolean);
    function ApplySrcChanges(Changes: TETSingleSrcChanges): boolean; // true if something changed
  public
    // properties
    property AutoHeaderBackground: TColor read FAutoHeaderBackground write SetAutoHeaderBackground default MsgWndDefAutoHeaderBackground;
    property BackgroundColor: TColor read FBackgroundColor write SetBackgroundColor default MsgWndDefBackgroundColor;
    property Color default clWindow;
    property FilenameStyle: TMsgWndFileNameStyle read FFilenameStyle write SetFilenameStyle;
    property HeaderBackground[aToolState: TLMVToolState]: TColor read GetHeaderBackground write SetHeaderBackground;
    property IdleConnected: boolean read FIdleConnected write SetIdleConnected;
    property Images: TCustomImageList read FImages write SetImages;
    property ItemHeight: integer read FItemHeight write SetItemHeight;
    property OnAllViewsStopped: TNotifyEvent read FOnAllViewsStopped write FOnAllViewsStopped;
    property OnOpenMessage: TOnOpenMessageLine read FOnOpenMessage write FOnOpenMessage;
    Property OnOptionsChanged: TNotifyEvent read FOnOptionsChanged write FOnOptionsChanged;
    property Options: TMsgCtrlOptions read FOptions write SetOptions default MCDefaultOptions;
    property SearchText: string read FSearchText write SetSearchText;
    property SelectedLine: integer read GetSelectedLine write SetSelectedLine; // -1=header line, can be on progress line (=View.Count)
    property SelectedView: TLMsgWndView read FSelectedView write SetSelectedView;
    property ShowHint default true;
    property SourceMarks: TETMarks read FSourceMarks write SetSourceMarks;
    property TextColor: TColor read FTextColor write SetTextColor default MsgWndDefTextColor;
    property UrgencyStyles[Urgency: TMessageLineUrgency]: TMsgCtrlUrgencyStyle read GetUrgencyStyles write SetUrgencyStyles;
  end;

  { TMessagesFrame }

  TMessagesFrame = class(TFrame)
    HideSearchSpeedButton: TSpeedButton;
    MsgCtrlPopupMenu: TPopupMenu;
    SearchEdit: TEdit;
    SearchNextSpeedButton: TSpeedButton;
    SearchPanel: TPanel;
    SearchPrevSpeedButton: TSpeedButton;
    procedure AboutToolMenuItemClick(Sender: TObject);
    procedure AddFilterMenuItemClick(Sender: TObject);
    procedure ClearFilterMsgTypesMenuItemClick(Sender: TObject);
    procedure ClearMenuItemClick(Sender: TObject);
    procedure CopyAllMenuItemClick(Sender: TObject);
    procedure CopyFilenameMenuItemClick(Sender: TObject);
    procedure CopyMsgMenuItemClick(Sender: TObject);
    procedure CopyShownMenuItemClick(Sender: TObject);
    procedure EditHelpMenuItemClick(Sender: TObject);
    procedure FileStyleMenuItemClick(Sender: TObject);
    procedure FindMenuItemClick(Sender: TObject);
    procedure HelpMenuItemClick(Sender: TObject);
    procedure FilterHintsWithoutPosMenuItemClick(Sender: TObject);
    procedure FilterMsgOfTypeMenuItemClick(Sender: TObject);
    procedure FilterUrgencyMenuItemClick(Sender: TObject);
    procedure HideSearchSpeedButtonClick(Sender: TObject);
    procedure MoreOptionsMenuItemClick(Sender: TObject);
    procedure MsgCtrlPopupMenuPopup(Sender: TObject);
    procedure OnSelectFilterClick(Sender: TObject);
    procedure OpenToolsOptionsMenuItemClick(Sender: TObject);
    procedure RemoveCompOptHideMsgClick(Sender: TObject);
    procedure SaveAllToFileMenuItemClick(Sender: TObject);
    procedure SaveShownToFileMenuItemClick(Sender: TObject);
    procedure SearchEditChange(Sender: TObject);
    procedure SearchEditKeyDown(Sender: TObject; var Key: Word;
      {%H-}Shift: TShiftState);
    procedure SearchNextSpeedButtonClick(Sender: TObject);
    procedure SearchPrevSpeedButtonClick(Sender: TObject);
    procedure ShowIDMenuItemClick(Sender: TObject);
    procedure SrcEditLinesChanged(Sender: TObject);
    procedure TranslateMenuItemClick(Sender: TObject);
    procedure RemoveFilterMsgTypeClick(Sender: TObject);
    procedure WndStayOnTopMenuItemClick(Sender: TObject);
  private
    function AllMessagesAsString(const OnlyShown: boolean): String;
    function GetAboutView: TLMsgWndView;
    function GetViews(Index: integer): TLMsgWndView;
    procedure HideSearch;
    procedure SaveClicked(OnlyShown: boolean);
    procedure CopyAllClicked(OnlyShown: boolean);
    procedure CopyMsgToClipboard(OnlyFilename: boolean);
    function GetMsgPattern(SubTool: string; MsgId: integer;
      WithUrgency: boolean; MaxLen: integer): string;
  protected
    procedure Notification(AComponent: TComponent; Operation: TOperation);
      override;
  public
    MessagesCtrl: TMessagesCtrl;
    constructor Create(TheOwner: TComponent); override;
    destructor Destroy; override;

    procedure ApplyIDEOptions;

    // Views
    function ViewCount: integer;
    property Views[Index: integer]: TLMsgWndView read GetViews;
    function GetView(aCaption: string; CreateIfNotExist: boolean): TLMsgWndView;
    function FindUnfinishedView: TLMsgWndView;
    procedure DeleteView(View: TLMsgWndView); // free view
    function IndexOfView(View: TLMsgWndView): integer;
    procedure ClearViews(OnlyFinished: boolean); // deletes/frees all views

    // source marks
    procedure CreateMarksForFile(aSynEdit: TSynEdit; aFilename: string;
      DeleteOld: boolean);
    procedure ApplySrcChanges(Changes: TETSingleSrcChanges);
    procedure ApplyMultiSrcChanges(Changes: TETMultiSrcChanges);
    procedure SourceEditorPopup(MarkLine: TSynEditMarkLine;
      const LogicalCaretXY: TPoint);
    procedure SourceEditorHint(MarkLine: TSynEditMarkLine;
      var HintStr: string);

    // message lines
    procedure SelectMsgLine(Msg: TMessageLine; DoScroll: boolean);
    function SelectFirstUrgentMessage(aMinUrgency: TMessageLineUrgency;
      WithSrcPos: boolean): boolean;
    function SelectNextUrgentMessage(aMinUrgency: TMessageLineUrgency;
      WithSrcPos, Downwards: boolean): boolean;
    procedure ClearCustomMessages(const ViewCaption: string='');
    function AddCustomMessage(TheUrgency: TMessageLineUrgency; Msg: string;
      aFilename: string = ''; LineNumber: integer = 0; Column: integer = 0;
      const ViewCaption: string = CustomViewCaption): TMessageLine;
  end;

const
  MessagesMenuRootName = 'Messages';
var
  MsgFindMenuItem: TIDEMenuCommand;
  MsgQuickFixMenuSection: TIDEMenuSection;
  MsgAboutSection: TIDEMenuSection;
    MsgAboutToolMenuItem: TIDEMenuCommand;
    MsgOpenToolOptionsMenuItem: TIDEMenuCommand;
  MsgFilterMsgOfTypeMenuItem: TIDEMenuCommand;
  MsgRemoveCompOptHideMenuSection: TIDEMenuSection;
  MsgRemoveMsgTypeFilterMenuSection: TIDEMenuSection;
    MsgRemoveFilterMsgOneTypeMenuSection: TIDEMenuSection;
    MsgRemoveFilterAllMsgTypesMenuItem: TIDEMenuCommand;
  MsgFilterBelowMenuSection: TIDEMenuSection;
    MsgFilterWarningsMenuItem: TIDEMenuCommand;
    MsgFilterNotesMenuItem: TIDEMenuCommand;
    MsgFilterHintsMenuItem: TIDEMenuCommand;
    MsgFilterVerboseMenuItem: TIDEMenuCommand;
    MsgFilterDebugMenuItem: TIDEMenuCommand;
    MsgFilterNoneMenuItem: TIDEMenuCommand;
  MsgFilterHintsWithoutPosMenuItem: TIDEMenuCommand;
  MsgFiltersMenuSection: TIDEMenuSection;
    MsgSelectFilterMenuSection: TIDEMenuSection;
    MsgAddFilterMenuItem: TIDEMenuCommand;
  MsgCopyMenuSection: TIDEMenuSection;
    MsgCopyFilenameMenuItem: TIDEMenuCommand;
    MsgCopyMsgMenuItem: TIDEMenuCommand;
    MsgCopyShownMenuItem: TIDEMenuCommand;
    MsgCopyAllMenuItem: TIDEMenuCommand;
  MsgSaveToFileMenuSection: TIDEMenuSection;
    MsgSaveAllToFileMenuItem: TIDEMenuCommand;
    MsgSaveShownToFileMenuItem: TIDEMenuCommand;
  MsgHelpMenuItem: TIDEMenuCommand;
  MsgEditHelpMenuItem: TIDEMenuCommand;
  MsgClearMenuItem: TIDEMenuCommand;
  MsgOptionsMenuSection: TIDEMenuSection;
    MsgWndStayOnTopMenuItem: TIDEMenuCommand;
    MsgFilenameStyleMenuSection: TIDEMenuSection;
      MsgFileStyleShortMenuItem: TIDEMenuCommand;
      MsgFileStyleRelativeMenuItem: TIDEMenuCommand;
      MsgFileStyleFullMenuItem: TIDEMenuCommand;
    MsgTranslateMenuItem: TIDEMenuCommand;
    MsgShowIDMenuItem: TIDEMenuCommand;
    MsgMoreOptionsMenuItem: TIDEMenuCommand;

procedure RegisterStandardMessagesViewMenuItems;

implementation

procedure RegisterStandardMessagesViewMenuItems;
var
  Parent: TIDEMenuSection;
  Root: TIDEMenuSection;
begin
  MessagesMenuRoot := RegisterIDEMenuRoot(MessagesMenuRootName);
  Root:=MessagesMenuRoot;
  MsgFindMenuItem := RegisterIDEMenuCommand(Root, 'Find', lisFind);
  MsgQuickFixMenuSection := RegisterIDEMenuSection(Root, 'Quick Fix');
  MsgAboutSection:=RegisterIDEMenuSection(Root,'About');
    Parent:=MsgAboutSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisAbout;
    MsgAboutToolMenuItem:=RegisterIDEMenuCommand(Parent, 'About', lisAbout);
    MsgOpenToolOptionsMenuItem:=RegisterIDEMenuCommand(Parent, 'Open Tool '
      +'Options', lisOpenToolOptions);
  MsgFilterMsgOfTypeMenuItem:=RegisterIDEMenuCommand(Root,'FilterMsgOfType',lisFilterAllMessagesOfCertainType);
  MsgRemoveCompOptHideMenuSection:=RegisterIDEMenuSection(Root,'RemoveCompOptHideMsg');
    Parent:=MsgRemoveCompOptHideMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisRemoveCompilerOptionHideMessage;
  MsgRemoveMsgTypeFilterMenuSection:=RegisterIDEMenuSection(Root,'RemoveMsgTypeFilters');
    Parent:=MsgRemoveMsgTypeFilterMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisRemoveMessageTypeFilter;
    MsgRemoveFilterMsgOneTypeMenuSection:=RegisterIDEMenuSection(Parent,'RemoveOneMsgTypeFilterSection');
    MsgRemoveFilterAllMsgTypesMenuItem:=RegisterIDEMenuCommand(Parent, 'Remove'
      +' all message type filters', lisRemoveAllMessageTypeFilters);
  MsgFilterBelowMenuSection:=RegisterIDEMenuSection(Root,'Filter Below Section');
    Parent:=MsgFilterBelowMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisFilterNonUrgentMessages;
    MsgFilterWarningsMenuItem:=RegisterIDEMenuCommand(Parent,
      'Filter Warnings', lisFilterWarningsAndBelow);
    MsgFilterWarningsMenuItem.RadioItem:=true;
    MsgFilterWarningsMenuItem.GroupIndex:=2;
    MsgFilterNotesMenuItem:=RegisterIDEMenuCommand(Parent, 'Filter Notes',
      lisFilterNotesAndBelow);
    MsgFilterNotesMenuItem.RadioItem:=true;
    MsgFilterNotesMenuItem.GroupIndex:=2;
    MsgFilterHintsMenuItem:=RegisterIDEMenuCommand(Parent, 'Filter Hints',
      lisFilterHintsAndBelow);
    MsgFilterHintsMenuItem.RadioItem:=true;
    MsgFilterHintsMenuItem.GroupIndex:=2;
    MsgFilterVerboseMenuItem:=RegisterIDEMenuCommand(Parent, 'Filter Verbose '
      +'Messages', lisFilterVerboseMessagesAndBelow);
    MsgFilterVerboseMenuItem.RadioItem:=true;
    MsgFilterVerboseMenuItem.GroupIndex:=2;
    MsgFilterDebugMenuItem:=RegisterIDEMenuCommand(Parent, 'Filter Debug '
      +'Messages', lisFilterDebugMessagesAndBelow);
    MsgFilterDebugMenuItem.RadioItem:=true;
    MsgFilterDebugMenuItem.GroupIndex:=2;
    MsgFilterNoneMenuItem:=RegisterIDEMenuCommand(Parent, 'Filter None, do not'
      +' filter by urgency', lisFilterNoneDoNotFilterByUrgency);
    MsgFilterNoneMenuItem.RadioItem:=true;
    MsgFilterNoneMenuItem.GroupIndex:=2;
  MsgFilterHintsWithoutPosMenuItem:=RegisterIDEMenuCommand(Root, 'Filter Hints'
    +' without Source Position', lisFilterHintsWithoutSourcePosition);
  MsgFiltersMenuSection:=RegisterIDEMenuSection(Root,'Switch Filter Section');
    Parent:=MsgFiltersMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisSwitchFilterSettings;
    MsgSelectFilterMenuSection:=RegisterIDEMenuSection(Parent,'Filters');
    MsgAddFilterMenuItem:=RegisterIDEMenuCommand(Parent, 'Add Filter',
      lisAddFilter);
  MsgCopyMenuSection:=RegisterIDEMenuSection(Root,'Copy');
    Parent:=MsgCopyMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisCopy;
    MsgCopyFilenameMenuItem:=RegisterIDEMenuCommand(Parent, 'Filename',
      lisCopyFileNameToClipboard);
    MsgCopyMsgMenuItem := RegisterIDEMenuCommand(Parent, 'Selected',lisCopySelectedMessagesToClipboard);
    MsgCopyShownMenuItem := RegisterIDEMenuCommand(Parent, 'Shown', lisCopyAllShownMessagesToClipboard);
    MsgCopyAllMenuItem:=RegisterIDEMenuCommand(Parent, 'All',
      lisCopyAllOriginalMessagesToClipboard);
  MsgSaveToFileMenuSection:=RegisterIDEMenuSection(Root,'Save');
    Parent:=MsgSaveToFileMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisSave;
    MsgSaveShownToFileMenuItem:=RegisterIDEMenuCommand(Parent, 'Save Shown '
      +'Messages to File', lisSaveShownMessagesToFile);
    MsgSaveAllToFileMenuItem:=RegisterIDEMenuCommand(Parent, 'Save All '
      +'Messages to File', lisSaveAllOriginalMessagesToFile);
  MsgHelpMenuItem := RegisterIDEMenuCommand(Root, 'Help for this message',lisHelp);
  MsgEditHelpMenuItem := RegisterIDEMenuCommand(Root, 'Edit help for messages',lisEditHelp);
  MsgClearMenuItem := RegisterIDEMenuCommand(Root, 'Clear', lisClear);
  MsgOptionsMenuSection:=RegisterIDEMenuSection(Root,'Option Section');
    Parent:=MsgOptionsMenuSection;
    Parent.ChildsAsSubMenu:=true;
    Parent.Caption:=lisOptions;
    MsgWndStayOnTopMenuItem:=RegisterIDEMenuCommand(Parent,
      'Window stay on top', lisWindowStaysOnTop);
    MsgFilenameStyleMenuSection:=RegisterIDEMenuSection(Parent,'Filename Styles');
      Parent:=MsgFilenameStyleMenuSection;
      Parent.ChildsAsSubMenu:=true;
      Parent.Caption:=lisFilenameStyle;
      MsgFileStyleShortMenuItem:=RegisterIDEMenuCommand(Parent, 'Short',
        lisShortNoPath);
      MsgFileStyleRelativeMenuItem:=RegisterIDEMenuCommand(Parent, 'Relative',
        lisRelative);
      MsgFileStyleFullMenuItem:=RegisterIDEMenuCommand(Parent, 'Full', lisFull);
    Parent:=MsgOptionsMenuSection;
    MsgTranslateMenuItem:=RegisterIDEMenuCommand(Parent, 'Translate',
      lisTranslateTheEnglishMessages);
    MsgShowIDMenuItem:=RegisterIDEMenuCommand(Parent, 'ShowID',
      lisShowMessageTypeID);
    MsgMoreOptionsMenuItem:=RegisterIDEMenuCommand(Parent, 'More Options',
      lisDlgMore);
end;

{$R *.lfm}

{ TLMsgWndView }

procedure TLMsgWndView.OnMarksFixed(ListOfTMessageLine: TFPList);
var
  i: Integer;
  ViewLine: TMessageLine;
  j: Integer;
  WorkerMsg: TMessageLine;
begin
  //debugln(['TLMsgWndView.OnMarksFixed START ',ListOfTMessageLine.Count]);
  // apply marks to WorkerMessages
  if Tool<>nil then begin
    Tool.EnterCriticalSection;
    try
      for i:=0 to ListOfTMessageLine.Count-1 do begin
        ViewLine:=TMessageLine(ListOfTMessageLine[i]);
        j:=Tool.WorkerMessages.IndexOfOutputIndex(ViewLine.OutputIndex);
        if j<0 then continue;
        WorkerMsg:=Tool.WorkerMessages[j];
        WorkerMsg.Flags:=ViewLine.Flags;
        //debugln(['TLMsgWndView.OnMarksFixed j=',j,' ',dbgs(WorkerMsg.Flags),' ',dbgs(Pointer(WorkerMsg)),' WorkerMsg.OutputIndex=',WorkerMsg.OutputIndex,' ViewLine.OutputIndex=',ViewLine.OutputIndex]);
      end;
    finally
      Tool.LeaveCriticalSection;
    end;
  end;

  // delete messages from view
  for i:=ListOfTMessageLine.Count-1 downto 0 do begin
    ViewLine:=TMessageLine(ListOfTMessageLine[i]);
    Lines.Delete(ViewLine);
  end;
  ListOfTMessageLine.Clear;

  // update control
  Control.UpdateScrollBar(true);
  Control.Invalidate;
end;

procedure TLMsgWndView.SetToolState(AValue: TLMVToolState);
begin
  if ToolState=AValue then Exit;
  inherited;
  Control.Invalidate;
end;

procedure TLMsgWndView.SetFilter(AValue: TLMsgViewFilter);
begin
  FFilter.Assign(AValue);
end;

procedure TLMsgWndView.FetchAllPending;
var
  OldLineCount: Integer;
  i: Integer;
  OldUpdateSortedSrcPos: Boolean;
  MsgLine: TMessageLine;
  Line: Integer;
  Col: Integer;
begin
  OldLineCount:=Lines.Count;
  inherited FetchAllPending;
  if OldLineCount=Lines.Count then exit;

  // apply pending src changes
  OldUpdateSortedSrcPos:=Lines.UpdateSortedSrcPos;
  if FPendingChanges.Count>0 then begin
    Lines.UpdateSortedSrcPos:=false;
    try
      for i:=OldLineCount to Lines.Count-1 do begin
        MsgLine:=Lines[i];
        //debugln(['TLMsgWndView.FetchAllPending ',i,' ',MsgLine.Msg]);
        Line:=MsgLine.Line;
        Col:=MsgLine.Column;
        FPendingChanges.AdaptCaret(MsgLine.GetFullFilename,Line,Col,
          mlfLeftToken in MsgLine.Flags);
        MsgLine.SetSourcePosition(MsgLine.Filename,Line,Col);
      end;
    finally
      Lines.UpdateSortedSrcPos:=OldUpdateSortedSrcPos;
    end;
  end;
end;

procedure TLMsgWndView.ToolExited;
var
  ErrCount: Integer;
  u: TMessageLineUrgency;
  MsgLine: TMessageLine;
  i: Integer;
  StartLine: Integer;
  sl: TStringList;
begin
  inherited ToolExited;
  if Tool.Terminated then begin
    ToolState:=lmvtsFailed;
  end else if (ExitStatus<>0) then begin
    // tool stopped with errors
    ErrCount:=0;
    EnterCriticalSection;
    try
      for u:=mluError to high(TMessageLineUrgency) do
        inc(ErrCount,Lines.UrgencyCounts[u]+PendingLines.UrgencyCounts[u]);
    finally
      LeaveCriticalSection;
    end;
    if ErrCount=0 then begin
      // parser did not add an error message
      // => add an error message
      // add the last 3 lines of output with fatal urgency
      Tool.EnterCriticalSection; // Note: always lock Tool before View
      try
        EnterCriticalSection;
        try
          StartLine:=Max(0,Tool.WorkerOutput.Count-100);
          if PendingLines.Count>0 then
            StartLine:=Max(StartLine,PendingLines[PendingLines.Count-1].OutputIndex+1);
          if Lines.Count>0 then
            StartLine:=Max(StartLine,Lines[Lines.Count-1].OutputIndex+1);
          for i:=StartLine to Tool.WorkerOutput.Count-1 do
          begin
            MsgLine:=PendingLines.CreateLine(-1);
            MsgLine.Msg:=Tool.WorkerOutput[i];
            MsgLine.Urgency:=mluPanic;
            PendingLines.Add(MsgLine);
          end;
          MsgLine:=PendingLines.CreateLine(-1);
          MsgLine.Urgency:=mluPanic;
          MsgLine.Msg:=Format(
            lisToolStoppedWithExitCodeUseContextMenuToGetMoreInfo, [IntToStr(
            ExitStatus)]);
          PendingLines.Add(MsgLine);
        finally
          LeaveCriticalSection;
        end;
      finally
        Tool.LeaveCriticalSection;
      end;
    end;
    ToolState:=lmvtsFailed;
  end else if Tool.ErrorMessage<>'' then begin
    // error executing the tool
    EnterCriticalSection;
    try
      sl:=TStringList.Create;
      try
        sl.Text:=Tool.ErrorMessage;
        for i:=0 to sl.Count-1 do begin
          if sl[i]='' then continue;
          MsgLine:=PendingLines.CreateLine(-1);
          MsgLine.Urgency:=mluPanic;
          MsgLine.Msg:=Format(lisInternalError, [sl[i]]);
          PendingLines.Add(MsgLine);
        end;
      finally
        sl.Free;
      end;
    finally
      LeaveCriticalSection;
    end;
    ToolState:=lmvtsFailed;
  end else
    ToolState:=lmvtsSuccess;
end;

constructor TLMsgWndView.Create(AOwner: TComponent);
begin
  fMessageLineClass:=TLMsgViewLine;
  inherited Create(AOwner);
  Lines.OnMarksFixed:=@OnMarksFixed;
  FFilter:=TLMsgViewFilter.Create;
  fPendingChanges:=TETMultiSrcChanges.Create(nil);
end;

destructor TLMsgWndView.Destroy;
begin
  inherited Destroy;
  FreeAndNil(FPendingChanges);
  FreeAndNil(FFilter);
end;

function TLMsgWndView.LineFits(Line: TMessageLine): boolean;
begin
  if FFilter<>nil then
    Result:=FFilter.LineFits(Line)
  else
    Result:=inherited LineFits(Line);
end;

function TLMsgWndView.HasContent: boolean;
begin
  Result:=GetShownLineCount(true,true)>0;
end;

function TLMsgWndView.GetShownLineCount(WithHeader, WithProgressLine: boolean
  ): integer;
begin
  Result:=Lines.Count;
  // the header is only shown if there SummaryMsg<>'' or ProgressLine.Msg<>'' or Lines.Count>0
  if ProgressLine.Msg<>'' then begin
    if WithHeader then
      inc(Result);
    if WithProgressLine then
      inc(Result);
  end else if Caption<>'' then begin
    if WithHeader then
      inc(Result);
  end else if (Result>0) and WithHeader then
    inc(Result);
end;

procedure TLMsgWndView.RebuildLines;
// called by main thread
var
  i: Integer;
  SrcMsg: TMessageLine;
  NewProgressLine: TMessageLine;
  NewMsg: TMessageLine;
  Line: Integer;
  Col: Integer;
begin
  if Tool=nil then exit;
  Tool.EnterCriticalSection; // lock Tool before View
  try
    EnterCriticalSection;
    try
      ClearLines;
      NewProgressLine:=nil;
      for i:=0 to Tool.WorkerMessages.Count-1 do begin
        SrcMsg:=Tool.WorkerMessages[i];
        //if Pos('"db"',SrcMsg.Msg)>0 then
         // debugln(['TLMsgWndView.RebuildLines i=',i,' Msg="',SrcMsg.Msg,'" Fits=',LineFits(SrcMsg),' ',dbgs(SrcMsg.Flags),' ',SrcMsg.OutputIndex]);
        if LineFits(SrcMsg) then begin
          NewProgressLine:=nil;
          NewMsg:=Lines.CreateLine(-1);
          NewMsg.Assign(SrcMsg);
          // adapt line,col due to src changes
          Line:=NewMsg.Line;
          Col:=NewMsg.Column;
          FPendingChanges.AdaptCaret(NewMsg.GetFullFilename,Line,Col,
                                     mlfLeftToken in NewMsg.Flags);
          NewMsg.SetSourcePosition(NewMsg.Filename,Line,Col);
          //debugln(['TLMsgWndView.RebuildLines NewMsg=',Lines.Count,'="',NewMsg.Msg,'"']);
          Lines.Add(NewMsg);
        end else begin
          NewProgressLine:=SrcMsg;
        end;
      end;
      FLastWorkerMessageCount:=Tool.WorkerMessages.Count-1;
      if (NewProgressLine<>nil) and Running then begin
        ProgressLine.Assign(NewProgressLine);
      end
      else if ProgressLine.Msg<>'' then begin
        ProgressLine.Clear;
      end;
    finally
      LeaveCriticalSection;
    end;
  finally
    Tool.LeaveCriticalSection;
  end;
end;

function TLMsgWndView.ApplySrcChanges(Changes: TETSingleSrcChanges): boolean;

  function ApplyChanges(CurChanges: TETSingleSrcChanges;
    CurLines: TMessageLines): boolean;
  var
    FromY: integer;
    MaxY: integer;
    LineDiffBehindMaxY: integer;
    ToY: Integer;
    MsgLine: TMessageLine;
    Line: Integer;
    Col: Integer;
    OldUpdateSortedSrcPos: Boolean;
  begin
    Result:=false;
    if CurChanges.First=nil then exit;
    CurChanges.GetRange(FromY,MaxY,LineDiffBehindMaxY);
    if LineDiffBehindMaxY=0 then
      ToY:=MaxY
    else
      ToY:=High(Integer);
    OldUpdateSortedSrcPos:=Lines.UpdateSortedSrcPos;
    CurLines.UpdateSortedSrcPos:=false;
    try
      {if CurLines=Lines then begin
        debugln(['ApplyChanges MinY=',FromY,' MaxY=',MaxY,' LineDiffBehindMaxY=',LineDiffBehindMaxY]);
        CurChanges.WriteDebugReport('Changes:');
      end;}
      for MsgLine in CurLines.EnumerateFile(CurChanges.Filename,FromY,ToY)
      do begin
        Line:=MsgLine.Line;
        Col:=MsgLine.Column;
        if Line>MaxY then
          inc(Line,LineDiffBehindMaxY)
        else
          CurChanges.AdaptCaret(Line,Col,mlfLeftToken in MsgLine.Flags);
        //if CurLines=Lines then
        //  debugln(['ApplyChanges ',MsgLine.Msg,' Old=',MsgLine.Line,',',MsgLine.Column,' New=',Line,',',Col]);

        if (Line=MsgLine.Line) and (MsgLine.Column=Col) then continue;
        MsgLine.SetSourcePosition(MsgLine.Filename,Line,Col);
        Result:=true;
      end;
    finally
      CurLines.UpdateSortedSrcPos:=OldUpdateSortedSrcPos;
    end;
  end;

var
  Queue: TETSingleSrcChanges;
  Change: TETSrcChange;
  Node: TAvgLvlTreeNode;
  aFilename: String;
begin
  Result:=false;
  //debugln(['TLMsgWndView.ApplySrcChanges START ',Changes.Filename,' ',Changes.First<>nil]);
  // check if there are changes
  if Changes.First=nil then exit;
  aFilename:=Changes.Filename;
  if aFilename='' then exit;

  // update visible lines
  Result:=ApplyChanges(Changes,Lines);

  // update pending lines
  if Tool<>nil then begin
    Tool.EnterCriticalSection; // lock Tool before View
    try
      EnterCriticalSection;
      try
        Queue:=PendingChanges.GetChanges(aFilename,true);
        Change:=Changes.First;
        while Change<>nil do begin
          Queue.Add(Change.Action,Change.FromPos,Change.ToPos);
          Change:=Change.Next;
        end;
        if not Running then begin
          // apply all pending changes to Tool.WorkerMessages
          Node:=PendingChanges.AllChanges.FindLowest;
          while Node<>nil do begin
            ApplyChanges(TETSingleSrcChanges(Node.Data),Tool.WorkerMessages);
            Node:=Node.Successor;
          end;
          PendingChanges.Clear;
        end;
      finally
        LeaveCriticalSection;
      end;
    finally
      Tool.LeaveCriticalSection;
    end;
  end;
end;

{ TMsgCtrlUrgencyStyle }

procedure TMsgCtrlUrgencyStyle.SetColor(AValue: TColor);
begin
  if FColor=AValue then Exit;
  FColor:=AValue;
  Changed;
end;

procedure TMsgCtrlUrgencyStyle.SetImageIndex(AValue: integer);
begin
  if FImageIndex=AValue then Exit;
  FImageIndex:=AValue;
  Changed;
end;

procedure TMsgCtrlUrgencyStyle.SetTranslated(AValue: string);
begin
  if FTranslated=AValue then Exit;
  FTranslated:=AValue;
  Changed;
end;

procedure TMsgCtrlUrgencyStyle.Changed;
begin
  Control.Invalidate;
end;

constructor TMsgCtrlUrgencyStyle.Create(AControl: TMessagesCtrl;
  TheUrgency: TMessageLineUrgency);
begin
  FControl:=AControl;
  fUrgency:=TheUrgency;
  FImageIndex:=-1;
  FColor:=clDefault;
end;

function TMsgCtrlUrgencyStyle.Equals(Obj: TObject): boolean;
var
  Src: TMsgCtrlUrgencyStyle;
begin
  if Obj is TMsgCtrlUrgencyStyle then begin
    Src:=TMsgCtrlUrgencyStyle(Obj);
    Result:=(ImageIndex=Src.ImageIndex)
        and (Color=Src.Color)
        and (Translated=Src.Translated);
  end else
    Result:=inherited Equals(Obj);
end;

procedure TMsgCtrlUrgencyStyle.Assign(Src: TMsgCtrlUrgencyStyle);
begin
  if Equals(Src) then exit;
  fImageIndex:=Src.ImageIndex;
  fColor:=Src.Color;
  fTranslated:=Src.Translated;
  Changed;
end;

procedure TMsgCtrlUrgencyStyle.SetValues(TheTranslated: string;
  TheImageIndex: integer; TheColor: TColor);
begin
  Translated:=TheTranslated;
  ImageIndex:=TheImageIndex;
  Color:=TheColor;
end;

{ TMessagesCtrl }

// inline
function TMessagesCtrl.ViewCount: integer;
begin
  Result:=FViews.Count;
end;

// inline
function TMessagesCtrl.Filters: TLMsgViewFilters;
begin
  Result:=EnvironmentOptions.MsgViewFilters;
end;

// inline
function TMessagesCtrl.GetActiveFilter: TLMsgViewFilter;
begin
  Result:=Filters.ActiveFilter;
end;

// inline
procedure TMessagesCtrl.SetActiveFilter(AValue: TLMsgViewFilter);
begin
  Filters.ActiveFilter:=AValue;
end;

function TMessagesCtrl.GetViews(Index: integer): TLMsgWndView;

  procedure RaiseOutOfBounds;
  begin
    raise Exception.Create('TMessagesCtrl.GetViews '+IntToStr(Index)+' out of bounds '+IntToStr(ViewCount));
  end;

begin
  if (Index<0) or (Index>=ViewCount) then
    RaiseOutOfBounds;
  Result:=TLMsgWndView(FViews[Index]);
end;

procedure TMessagesCtrl.OnViewChanged(Sender: TObject);
var
  AllViewsStopped: Boolean;
  i: Integer;
begin
  for i:=0 to ViewCount-1 do begin
    if Views[i].Running then begin
      // the views may change many times
      // reduce the update of the control to a few per second by using a timer
      fSomeViewsRunning:=true;
      FUpdateTimer.Enabled:=true;
      exit;
    end;
  end;
  // no views are running
  // The variable fSomeViewsRunning contains the last state
  // if fSomeViewsRunning was true, then all views have stopped
  AllViewsStopped:=fSomeViewsRunning;
  fSomeViewsRunning:=false;
  // no views running => update immediately
  FetchNewMessages;

  if AllViewsStopped then
    DoAllViewsStopped;
end;

procedure TMessagesCtrl.FetchNewMessages;
// called when new messages are available from the worker threads
// calls Views to fetch and filter new messages
// scrolls to new message
var
  i: Integer;
begin
  if csDestroying in ComponentState then exit;
  BeginUpdate;
  try
    for i:=0 to ViewCount-1 do
      FetchNewMessages(Views[i]);
  finally
    EndUpdate;
  end;
  UpdateScrollBar(true);
end;

function TMessagesCtrl.FetchNewMessages(View: TLMsgWndView): boolean;
var
  OldLineCount: Integer;
  i: Integer;
  OtherView: TLMsgWndView;
  MaxY: Integer;
  y: Integer;
begin
  Result:=false;
  if csDestroying in ComponentState then exit;
  if IndexOfView(View)<0 then exit;

  OldLineCount:=View.Lines.Count;
  if not View.ApplyPending then
    exit; // no new lines
  Result:=true;
  CreateSourceMarks(View,OldLineCount);
  UpdateScrollBar(true);
  Invalidate;

  // auto scroll
  if (SelectedView<>nil)
  and (SelectedLine<SelectedView.Lines.Count) then
    exit; // user has selected a non progress line -> do not auto scroll

  for i:=0 to ViewCount-1 do
  begin
    OtherView:=Views[i];
    if OtherView=View then break;
    if OtherView.Running then begin
      // there is still a prior View running
      // -> keep the last line of the other View visible
      MaxY:=GetLineTop(OtherView,OtherView.GetShownLineCount(true,true),false);
      y:=GetLineTop(View,View.GetShownLineCount(false,true),false);
      ScrollTop:=Min(MaxY,y);
      exit;
    end;
  end;
  // scroll to last line
  ScrollToLine(View,View.GetShownLineCount(false,true),true);
end;

procedure TMessagesCtrl.MsgUpdateTimerTimer(Sender: TObject);
begin
  FUpdateTimer.Enabled:=false;
  FetchNewMessages;
end;

procedure TMessagesCtrl.SetBackgroundColor(AValue: TColor);
begin
  if FBackgroundColor=AValue then Exit;
  FBackgroundColor:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetFilenameStyle(AValue: TMsgWndFileNameStyle);
begin
  if FFilenameStyle=AValue then Exit;
  FFilenameStyle:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetHeaderBackground(aToolState: TLMVToolState;
  AValue: TColor);
begin
  if FHeaderBackground[aToolState]=AValue then exit;
  FHeaderBackground[aToolState]:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetIdleConnected(AValue: boolean);
begin
  if FIdleConnected=AValue then Exit;
  FIdleConnected:=AValue;
  if IdleConnected then
    Application.AddOnIdleHandler(@OnIdle)
  else
    Application.RemoveOnIdleHandler(@OnIdle);
end;

procedure TMessagesCtrl.SetImages(AValue: TCustomImageList);
begin
  if FImages=AValue then Exit;
  if Images <> nil then
    Images.UnRegisterChanges(FImageChangeLink);
  FImages:=AValue;
  if Images <> nil then begin
    Images.RegisterChanges(FImageChangeLink);
    Images.FreeNotification(Self);
    if ItemHeight<Images.Height+2 then
      ItemHeight:=Images.Height+2;
  end;
  Invalidate;
end;

procedure TMessagesCtrl.SetItemHeight(AValue: integer);
begin
  FItemHeight:=Max(0,FItemHeight);
  if FItemHeight=AValue then Exit;
  FItemHeight:=AValue;
  UpdateScrollBar(true);
  Invalidate;
end;

procedure TMessagesCtrl.SetOptions(NewOptions: TMsgCtrlOptions);
var
  ChangedOptions: TMsgCtrlOptions;
begin
  if FOptions=NewOptions then Exit;
  ChangedOptions:=(FOptions-NewOptions)+(NewOptions-FOptions);
  FOptions:=NewOptions;
  if [mcoShowStats,mcoShowTranslated,mcoShowMessageID,mcoShowMsgIcons,
    mcoAlwaysDrawFocused]*ChangedOptions<>[]
  then
    Invalidate;
  if Assigned(OnOptionsChanged) then
    OnOptionsChanged(Self);
end;

procedure TMessagesCtrl.SetScrollLeft(AValue: integer);
begin
  AValue:=Max(0,Min(AValue,ScrollLeftMax));
  if FScrollLeft=AValue then Exit;
  FScrollLeft:=AValue;
  UpdateScrollBar(false);
  Invalidate;
end;

procedure TMessagesCtrl.SetScrollTop(AValue: integer);
begin
  AValue:=Max(0,Min(AValue,ScrollTopMax));
  if FScrollTop=AValue then Exit;
  FScrollTop:=AValue;
  UpdateScrollBar(false);
  Invalidate;
end;

procedure TMessagesCtrl.SetSearchText(AValue: string);
begin
  if FSearchText=AValue then Exit;
  FSearchText:=AValue;
  IdleConnected:=true;
end;

procedure TMessagesCtrl.SetSelectedLine(AValue: integer);
begin
  if AValue<-1 then AValue:=-1;
  if FSelectedLine=AValue then Exit;
  AValue:=Min(AValue,GetMaxSelectedLine);
  if FSelectedLine=AValue then Exit;
  FSelectedLine:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetSelectedView(AValue: TLMsgWndView);
begin
  if FSelectedView=AValue then Exit;
  FSelectedView:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetSourceMarks(AValue: TETMarks);
begin
  if FSourceMarks=AValue then Exit;
  FSourceMarks:=AValue;
  if SourceMarks<>nil then
    FreeNotification(SourceMarks);
end;

procedure TMessagesCtrl.SetTextColor(AValue: TColor);
begin
  if FTextColor=AValue then Exit;
  FTextColor:=AValue;
  Invalidate;
end;

procedure TMessagesCtrl.SetUrgencyStyles(Urgency: TMessageLineUrgency;
  AValue: TMsgCtrlUrgencyStyle);
begin
  fUrgencyStyles[Urgency].Assign(AValue);
end;

procedure TMessagesCtrl.SetAutoHeaderBackground(AValue: TColor);
begin
  if FAutoHeaderBackground=AValue then Exit;
  FAutoHeaderBackground:=AValue;
  Invalidate;
end;

function TMessagesCtrl.UrgencyToStr(Urgency: TMessageLineUrgency): string;
begin
  if (mcoShowTranslated in Options)
  and (fUrgencyStyles[Urgency].Translated<>'') then
    Result:=fUrgencyStyles[Urgency].Translated
  else
    Result:=MessageLineUrgencyNames[Urgency];
end;

procedure TMessagesCtrl.WMHScroll(var Msg: TLMScroll);
begin
  case Msg.ScrollCode of
      // Scrolls to start / end of the line
    SB_TOP: ScrollLeft := 1;
    SB_BOTTOM: ScrollLeft := ScrollLeftMax;
      // Scrolls one char left / right
    SB_LINEDOWN: ScrollLeft := ScrollLeft + 1;
    SB_LINEUP: ScrollLeft := ScrollLeft - 1;
      // Scrolls one page of chars left / right
    SB_PAGEDOWN: ScrollLeft := ScrollLeft + (ClientWidth div 2);
    SB_PAGEUP: ScrollLeft := ScrollLeft - (ClientHeight div 2);
      // Scrolls to the current scroll bar position
    SB_THUMBPOSITION,
    SB_THUMBTRACK: ScrollLeft := Msg.Pos;
  end;
end;

procedure TMessagesCtrl.WMVScroll(var Msg: TLMScroll);
begin
  case Msg.ScrollCode of
      // Scrolls to start / end of the text
    SB_TOP:        ScrollTop := 0;
    SB_BOTTOM:     ScrollTop := ScrollTopMax;
      // Scrolls one line up / down
    SB_LINEDOWN:   ScrollTop := ScrollTop + ItemHeight div 2;
    SB_LINEUP:     ScrollTop := ScrollTop - ItemHeight div 2;
      // Scrolls one page of lines up / down
    SB_PAGEDOWN:   ScrollTop := ScrollTop + ClientHeight - ItemHeight;
    SB_PAGEUP:     ScrollTop := ScrollTop - ClientHeight + ItemHeight;
      // Scrolls to the current scroll bar position
    SB_THUMBPOSITION,
    SB_THUMBTRACK: ScrollTop := Msg.Pos;
      // Ends scrolling
    SB_ENDSCROLL:  SetCaptureControl(nil); // release scrollbar capture
  end;
end;

procedure TMessagesCtrl.WMMouseWheel(var Message: TLMMouseEvent);
begin
  if Mouse.WheelScrollLines=-1 then
  begin
    // -1 : scroll by page
    ScrollTop := ScrollTop -
              (Message.WheelDelta * (ClientHeight - ItemHeight)) div 120;
  end else begin
    // scrolling one line -> scroll half an item, see SB_LINEDOWN and SB_LINEUP
    // handler in WMVScroll
    ScrollTop := ScrollTop -
        (Message.WheelDelta * Mouse.WheelScrollLines*ItemHeight) div 240;
  end;
  Message.Result := 1;
end;

procedure TMessagesCtrl.WMSetFocus(var Message: TLMSetFocus);
begin
  Invalidate;
  inherited;
end;

procedure TMessagesCtrl.WMKillFocus(var Message: TLMKillFocus);
begin
  Invalidate;
  inherited;
end;

function TMessagesCtrl.GetMaxSelectedLine: integer;
var
  View: TLMsgWndView;
begin
  View:=SelectedView;
  if View<>nil then
    Result:=View.GetShownLineCount(false,true)-1
  else
    Result:=-1;
end;

procedure TMessagesCtrl.ImageListChange(Sender: TObject);
begin
  Invalidate;
end;

procedure TMessagesCtrl.OnIdle(Sender: TObject; var Done: Boolean);
var
  View: TLMsgWndView;
  LineNumber: integer;
  i: Integer;
begin
  //debugln(['TMessagesCtrl.OnIdle fLastLoSearchText=',fLastLoSearchText,' ',UTF8LowerCase(fSearchText)]);
  for i:=0 to ViewCount-1 do begin
    View:=Views[i];
    if not View.Filter.IsEqual(ActiveFilter) then begin
      View.EnterCriticalSection;
      try
        View.Filter:=ActiveFilter;
      finally
        View.LeaveCriticalSection;
      end;
      View.RebuildLines;
      CreateSourceMarks(View,0);
      UpdateScrollBar(true);
      Invalidate;
    end;
  end;

  if fLastLoSearchText<>UTF8LowerCase(fSearchText) then begin
    fLastLoSearchText:=UTF8LowerCase(FSearchText);
    if SearchNext(fLastSearchStartView,fLastSearchStartLine,false,true,
      View,LineNumber)
    then begin
      //debugln(['TMessagesCtrl.OnIdle search text found ',LineNumber]);
      Select(View,LineNumber,true,true);
    end else begin
      //debugln(['TMessagesCtrl.OnIdle search text not found']);
    end;
    Invalidate;
  end;
  IdleConnected:=false;
end;

procedure TMessagesCtrl.OnFilterChanged(Sender: TObject);
begin
  IdleConnected:=true;
end;

function TMessagesCtrl.GetSelectedLine: integer;
var
  View: TLMsgWndView;
begin
  View:=SelectedView;
  if View<>nil then begin
    Result:=FSelectedLine;
    if Result>=0 then
      Result:=Min(FSelectedLine,GetMaxSelectedLine);
  end else begin
    Result:=-1;
  end;
  FSelectedLine:=Result;
end;

procedure TMessagesCtrl.CreateSourceMarks(View: TLMsgWndView;
  StartLineNumber: Integer);
var
  i: Integer;
begin
  if SourceMarks=nil then exit;
  for i:=StartLineNumber to View.Lines.Count-1 do
    CreateSourceMark(View.Lines[i],nil);
end;

function TMessagesCtrl.GetHeaderBackground(aToolState: TLMVToolState): TColor;
begin
  Result:=FHeaderBackground[aToolState];
end;

procedure TMessagesCtrl.CreateSourceMark(MsgLine: TMessageLine;
  aSynEdit: TSynEdit);
var
  SourceMark: TETMark;
begin
  if TLMsgViewLine(MsgLine).Mark<>nil then exit;
  if ord(MsgLine.Urgency)<ord(mluHint) then exit;
  SourceMark:=SourceMarks.CreateMark(MsgLine,aSynEdit);
  if SourceMark=nil then exit;
  TLMsgViewLine(MsgLine).Mark:=SourceMark;
end;

function TMessagesCtrl.GetUrgencyStyles(Urgency: TMessageLineUrgency
  ): TMsgCtrlUrgencyStyle;
begin
  Result:=fUrgencyStyles[Urgency];
end;

procedure TMessagesCtrl.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if Operation=opRemove then begin
    if (AComponent is TLMsgWndView) and (FViews.IndexOf(AComponent)>=0) then begin
      if fLastSearchStartView=AComponent then
        fLastSearchStartView:=nil;
      if SelectedView=AComponent then
        FSelectedView:=nil;
      RemoveView(TLMsgWndView(AComponent));
    end
    else if AComponent=Images then
      Images:=nil
    else if AComponent=SourceMarks then
      SourceMarks:=nil;
  end;
end;

procedure TMessagesCtrl.Paint;
var
  LoSearchText: string;

  procedure DrawText(ARect: TRect; aTxt: string; IsSelected: boolean;
    TxtColor: TColor);
  var
    Details: TThemedElementDetails;
    TextRect: TRect;
    p: SizeInt;
    LoTxt: String;
    aLeft: Integer;
    aRight: Integer;
    LastP: Integer;
  begin
    Canvas.Font.Color:=Font.Color;
    TextRect:=ARect;
    TextRect.Right:=TextRect.Left+Canvas.TextWidth(aTxt)+2;
    if IsSelected then begin
      if (mcsFocused in FStates) or (mcoAlwaysDrawFocused in Options) then
        Details := ThemeServices.GetElementDetails(ttItemSelected)
      else
        Details := ThemeServices.GetElementDetails(ttItemSelectedNotFocus);
      ThemeServices.DrawElement(Canvas.Handle, Details, TextRect, nil);
      TxtColor:=clDefault;
    end else
      Details := ThemeServices.GetElementDetails(ttItemNormal);
    if LoSearchText<>'' then begin
      LoTxt:=UTF8LowerCase(aTxt);
      p:=1;
      LastP:=1;
      while p<=length(LoTxt) do begin
        p:=PosEx(LoSearchText,LoTxt,LastP);
        if p<1 then break;
        Canvas.Brush.Color:=clHighlight;
        aLeft:=TextRect.Left+Canvas.TextWidth(copy(ATxt,1,p-1));
        aRight:=aLeft+Canvas.TextWidth(copy(ATxt,p,length(LoSearchText)));
        Canvas.FillRect(aLeft,TextRect.Top+1,aRight,TextRect.Bottom-1);
        LastP:=p+length(LoSearchText);
      end;
      Canvas.Brush.Color:=BackgroundColor;
    end;
    if TxtColor=clDefault then
      ThemeServices.DrawText(Canvas, Details, ATxt, TextRect,
        DT_CENTER or DT_VCENTER or DT_SINGLELINE or DT_NOPREFIX, 0)
    else begin
      p:=(TextRect.Top+TextRect.Bottom-Canvas.TextHeight('Mg')) div 2;
      Canvas.Font.Color:=TxtColor;
      Canvas.TextOut(TextRect.Left+2,p,ATxt);
    end;
  end;

var
  i: Integer;
  View: TLMsgWndView;
  y: Integer;
  j: Integer;
  Line: TMessageLine;
  Indent: Integer;
  NodeRect: TRect;
  ImgIndex: LongInt;
  IsSelected: Boolean;
  FirstLineIsNotSelectedMessage: Boolean;
  SecondLineIsNotSelectedMessage: Boolean;
  col: TColor;
begin
  if Focused then
    Include(FStates,mcsFocused)
  else
    Exclude(FStates,mcsFocused);
  //debugln(['TMessagesCtrl.Paint ',Focused,' CanFocus=',CanFocus,' TabStop=',TabStop]);
  LUIncreaseChangeStamp64(FPaintStamp);

  // paint background
  Canvas.Brush.Color:=BackgroundColor;
  Canvas.FillRect(0,0,ClientWidth,ClientHeight);

  Indent:=BorderWidth+2;
  LoSearchText:=fLastLoSearchText;

  // paint from top to bottom
  y:=-ScrollTop;
  for i:=0 to ViewCount-1 do begin
    if y>ClientHeight then break;
    View:=Views[i];
    if not View.HasContent then continue;

    View.FPaintStamp:=FPaintStamp;
    View.fPaintTop:=y;

    // draw header
    if (y+ItemHeight>0) and (y<ClientHeight) then begin
      // header text
      NodeRect:=Rect(0,y,ClientWidth,y+ItemHeight);
      Canvas.Brush.Color:=HeaderBackground[View.ToolState];
      Canvas.FillRect(NodeRect);
      Canvas.Pen.Style:=psDash;
      Canvas.Line(NodeRect.Left,NodeRect.Top,NodeRect.Right,NodeRect.Top);
      Canvas.Pen.Style:=psSolid;
      DrawText(NodeRect,GetHeaderText(View),
        (fSelectedView=View) and (FSelectedLine=-1),TextColor);
      Canvas.Brush.Color:=BackgroundColor;
    end;
    inc(y,ItemHeight);

    // draw lines
    j:=0;
    if y<0 then begin
      j:=Min((-y) div ItemHeight,View.Lines.Count);
      inc(y,j*ItemHeight);
    end;
    FirstLineIsNotSelectedMessage:=false;
    SecondLineIsNotSelectedMessage:=false;
    while (j<View.Lines.Count) and (y<ClientHeight) do begin
      Line:=View.Lines[j];
      NodeRect:=Rect(Indent,y,ClientWidth,y+ItemHeight);
      IsSelected:=(fSelectedView=View) and (FSelectedLine=j);
      if not IsSelected then begin
        if (y>-ItemHeight) and (y<=0) then
          FirstLineIsNotSelectedMessage:=true
        else if (y>0) and (y<=ItemHeight) then
          SecondLineIsNotSelectedMessage:=true;
      end;
      ImgIndex:=fUrgencyStyles[Line.Urgency].ImageIndex;
      if (Images<>nil) and (mcoShowMsgIcons in Options)
      and (ImgIndex>=0) and (ImgIndex<Images.Count) then begin
        Images.Draw(Canvas,
          NodeRect.Left + 1, (NodeRect.Top + NodeRect.Bottom - Images.Height) div 2,
          ImgIndex, gdeNormal);
        inc(NodeRect.Left,Images.Width+2);
      end;
      // message text
      col:=UrgencyStyles[Line.Urgency].Color;
      if col=clDefault then
        col:=TextColor;
      DrawText(NodeRect,GetLineText(Line),IsSelected,col);
      inc(y,ItemHeight);
      inc(j);
    end;
    if FirstLineIsNotSelectedMessage and SecondLineIsNotSelectedMessage then begin
      // the first two lines are normal messages, not selected
      // => paint view header hint
      NodeRect:=Rect(0,0,ClientWidth,ItemHeight div 2);
      Canvas.Brush.Color:=HeaderBackground[View.ToolState];
      Canvas.Brush.Style:=bsSolid;
      Canvas.FillRect(NodeRect);
      NodeRect:=Rect(0,NodeRect.Bottom,ClientWidth,ItemHeight);
      Canvas.GradientFill(NodeRect,HeaderBackground[View.ToolState],
        AutoHeaderBackground,gdVertical);
      Canvas.Pen.Style:=psDash;
      NodeRect:=Rect(0,0,ClientWidth,ItemHeight);
      Canvas.Line(NodeRect.Left,NodeRect.Bottom,NodeRect.Right,NodeRect.Bottom);
      Canvas.Pen.Style:=psSolid;
      DrawText(NodeRect,'...'+GetHeaderText(View),false,TextColor);
      Canvas.Brush.Color:=BackgroundColor;
    end;
    inc(y,ItemHeight*(View.Lines.Count-j));

    // draw progress line
    if View.ProgressLine.Msg<>'' then begin
      if (y+ItemHeight>0) and (y<ClientHeight) then begin
        // progress text
        NodeRect:=Rect(Indent,y,ClientWidth,y+ItemHeight);
        col:=UrgencyStyles[View.ProgressLine.Urgency].Color;
        if col=clDefault then
          col:=TextColor;
        DrawText(NodeRect,View.ProgressLine.Msg,
          (fSelectedView=View) and (FSelectedLine=View.Lines.Count),col);
      end;
      inc(y,ItemHeight);
    end;

    View.fPaintBottom:=y;
  end;

  // call OnPaint
  inherited Paint;
end;

procedure TMessagesCtrl.UpdateScrollBar(InvalidateScrollMax: boolean);
var
  ScrollInfo: TScrollInfo;
begin
  if InvalidateScrollMax then begin
    fScrollTopMax:=-1;
  end;
  if not HandleAllocated then exit;

  ScrollInfo.cbSize := SizeOf(ScrollInfo);
  ScrollInfo.fMask := SIF_ALL or SIF_DISABLENOSCROLL;
  ScrollInfo.nMin := 0;
  ScrollInfo.nTrackPos := 0;
  ScrollInfo.nMax := ScrollTopMax+ClientHeight-1;
  if ClientHeight < 2 then
    ScrollInfo.nPage := 1
  else
    ScrollInfo.nPage := ClientHeight-1;
  if ScrollTop > ScrollTopMax then
    ScrollTop := ScrollTopMax;
  ScrollInfo.nPos := ScrollTop;
  //debugln(['TMessagesCtrl.UpdateScrollBar ScrollTop=',ScrollTop,' ScrollTopMax=',ScrollTopMax]);
  ShowScrollBar(Handle, SB_VERT, True);
  SetScrollInfo(Handle, SB_VERT, ScrollInfo, false);
end;

procedure TMessagesCtrl.CreateWnd;
begin
  inherited CreateWnd;
  ItemHeight:=Canvas.TextHeight('Mg')+2;
  UpdateScrollBar(false);
end;

procedure TMessagesCtrl.DoSetBounds(ALeft, ATop, AWidth, AHeight: integer);
begin
  inherited DoSetBounds(ALeft, ATop, AWidth, AHeight);
  UpdateScrollBar(true);
end;

procedure TMessagesCtrl.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
  inherited MouseMove(Shift, X, Y);
  //Application.HideHint;
end;

procedure TMessagesCtrl.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
var
  View: TLMsgWndView;
  LineNumber: integer;
begin
  if not Focused and CanFocus then
    SetFocus;
  inherited MouseDown(Button, Shift, X, Y);
  if GetLineAt(Y,View,LineNumber) then begin
    if Button=mbLeft then begin
      Select(View,LineNumber,true,true);
      StoreSelectedAsSearchStart;

      if ((ssDouble in Shift) and (not (mcoSingleClickOpensFile in FOptions)))
      or ((mcoSingleClickOpensFile in FOptions) and ([ssDouble,ssTriple,ssQuad]*Shift=[]))
      then
        OpenSelection;
    end else if Button=mbRight then begin
      if not IsLineSelected(View,LineNumber) then begin
        Select(View,LineNumber,true,true);
        StoreSelectedAsSearchStart;
      end;
    end;
  end;
end;

procedure TMessagesCtrl.KeyDown(var Key: Word; Shift: TShiftState);
begin
  inherited KeyDown(Key, Shift);

  case Key of

  VK_DOWN:
    begin
      SelectNextShown(+1);
      Key:=VK_UNKNOWN;
    end;

  VK_UP:
    begin
      SelectNextShown(-1);
      Key:=VK_UNKNOWN;
    end;

  VK_HOME:
    begin
      SelectFirst(true,true);
      Key:=VK_UNKNOWN;
    end;

  VK_END:
    begin
      SelectLast(true,true);
      Key:=VK_UNKNOWN;
    end;

  VK_PRIOR: // Page Up
    begin
      SelectNextShown(-1-Max(0,ClientHeight div ItemHeight));
      Key:=VK_UNKNOWN;
    end;

  VK_NEXT: // Page Down
    begin
      SelectNextShown(1+Max(0,ClientHeight div ItemHeight));
      Key:=VK_UNKNOWN;
    end;
  end;
end;

procedure TMessagesCtrl.DoOnShowHint(HintInfo: PHintInfo);
var
  View: TLMsgWndView;
  Line: integer;
  MsgLine: TMessageLine;
  s: String;
begin
  if GetLineAt(HintInfo^.CursorPos.Y,View,Line) then begin
    MsgLine:=nil;
    s:='';
    if Line<0 then
      s:=GetHeaderText(View)
    else if Line<View.Lines.Count then begin
      MsgLine:=View.Lines[Line];
    end else begin
      MsgLine:=View.ProgressLine;
    end;
    if MsgLine<>nil then begin
      s:=GetLineText(MsgLine);
      s+=LineEnding+LineEnding;
      s+=ExternalToolList.GetMsgHint(MsgLine.SubTool,MsgLine.MsgID);
    end;
    HintInfo^.HintStr:=s;
    HintInfo^.ReshowTimeout:=0;
    HintInfo^.HideTimeout:=5000;
  end;
  inherited DoOnShowHint(HintInfo);
end;

procedure TMessagesCtrl.DoAllViewsStopped;
var
  CurLine: TMessageLine;
begin
  if Assigned(OnAllViewsStopped) then
    OnAllViewsStopped(Self);
  if mcoAutoOpenFirstError in Options then begin
    CurLine:=GetSelectedMsg;
    if (CurLine<>nil) and (CurLine.Urgency>=mluError)
    and CurLine.HasSourcePosition then
      exit;
    if SelectFirstUrgentMessage(mluError,true) then
      OpenSelection;
  end;
end;

function TMessagesCtrl.SearchNext(StartView: TLMsgWndView; StartLine: integer;
  SkipStart, Downwards: boolean; out View: TLMsgWndView; out LineNumber: integer
  ): boolean;
var
  CurView: TLMsgWndView;
  CurLine: Integer;
  CurViewLineCnt: integer;
  Txt: String;

  function Next: boolean;
  var
    i: Integer;
  begin
    if Downwards then begin
      inc(CurLine);
      if CurLine>=CurViewLineCnt then begin
        i:=IndexOfView(CurView);
        repeat
          inc(i);
          if i>=ViewCount then exit(false);
          CurView:=Views[i];
        until CurView.HasContent;
        CurLine:=-1;
        CurViewLineCnt:=CurView.GetShownLineCount(true,true);
      end;
    end else begin
      dec(CurLine);
      if CurLine<-1 then begin
        i:=IndexOfView(CurView);
        repeat
          dec(i);
          if i<0 then exit(false);
          CurView:=Views[i];
        until CurView.HasContent;
        CurViewLineCnt:=CurView.GetShownLineCount(true,true);
        CurLine:=CurViewLineCnt-1;
      end;
    end;
    Result:=true;
  end;

begin
  Result:=false;
  View:=nil;
  LineNumber:=-1;
  if ViewCount=0 then exit;
  if StartView=nil then begin
    // use default start
    if Downwards then begin
      StartView:=Views[0];
      StartLine:=-1;
    end else begin
      StartView:=Views[ViewCount-1];
      StartLine:=StartView.GetShownLineCount(true,true);
    end;
  end;
  CurView:=StartView;
  CurLine:=StartLine;
  CurViewLineCnt:=CurView.GetShownLineCount(true,true);
  // skip invalid line numbers
  if CurLine<-1 then begin
    SkipStart:=false;
    if Downwards then
      CurLine:=-1
    else if not Next then
      exit;
  end else if CurLine>=CurViewLineCnt then begin
    SkipStart:=false;
    if Downwards then begin
      if not Next then exit;
    end else
      CurLine:=CurViewLineCnt-1;
  end;
  // skip invalid views
  if not CurView.HasContent then begin
    SkipStart:=false;
    if not Next then exit;
  end;
  // skip start
  if SkipStart then
    if not Next then exit;
  // search
  repeat
    if CurLine<0 then
      Txt:=GetHeaderText(CurView)
    else if CurLine<CurView.Lines.Count then
      Txt:=GetLineText(CurView.Lines[CurLine])
    else
      Txt:=CurView.ProgressLine.Msg;
    Txt:=UTF8LowerCase(Txt);
    if Pos(fLastLoSearchText,Txt)>0 then begin
      View:=CurView;
      LineNumber:=CurLine;
      exit(true);
    end;
  until not Next;
end;

procedure TMessagesCtrl.Select(Msg: TMessageLine; DoScroll: boolean);
begin
  BeginUpdate;
  if (Msg=nil) or (Msg.Lines=nil) or (not (Msg.Lines.Owner is TLMsgWndView))
  then begin
    SelectedView:=nil;
    SelectedLine:=-1;
  end else begin
    SelectedView:=TLMsgWndView(Msg.Lines.Owner);
    SelectedLine:=Msg.Index;
    if DoScroll then
      ScrollToLine(SelectedView,SelectedLine,true);
  end;
  EndUpdate;
end;

function TMessagesCtrl.SelectNextOccurence(Downwards: boolean): boolean;
var
  View: TLMsgWndView;
  LineNumber: integer;
begin
  StoreSelectedAsSearchStart;
  Result:=SearchNext(SelectedView,SelectedLine,true,Downwards,View,LineNumber);
  if not Result then exit;
  Select(View,LineNumber,true,true);
end;

function TMessagesCtrl.SelectNextShown(Offset: integer): boolean;
// returns true if selection changed
var
  View: TLMsgWndView;
  Line: Integer;
  i: Integer;
begin
  Result:=false;
  {$IFDEF VerboseMsgCtrlSelectNextShown}
  debugln(['TMessagesCtrl.SelectNextShown START']);
  {$ENDIF}
  while Offset<>0 do begin
    {$IFDEF VerboseMsgCtrlSelectNextShown}
    debugln(['TMessagesCtrl.SelectNextShown LOOP Offset=',Offset,' ViewIndex=',IndexOfView(SelectedView),' Line=',SelectedLine]);
    {$ENDIF}
    if SelectedView=nil then begin
      if Offset>0 then begin
        SelectFirst(true,true);
        dec(Offset);
      end else begin
        SelectLast(true,true);
        Inc(Offset);
      end;
      Result:=true;
    end else begin
      View:=SelectedView;
      Line:=SelectedLine;
      if Offset>0 then begin
        {$IFDEF VerboseMsgCtrlSelectNextShown}
        debugln(['TMessagesCtrl.SelectNextShown NEXT View.GetShownLineCount(false,true)=',View.GetShownLineCount(false,true),' ',' ViewIndex=',IndexOfView(View),' Line=',Line]);
        {$ENDIF}
        inc(Line,Offset);
        if Line<View.GetShownLineCount(false,true) then
          Offset:=0
        else begin
          // next view
          Offset:=Line-View.GetShownLineCount(false,true);
          i:=IndexOfView(View);
          {$IFDEF VerboseMsgCtrlSelectNextShown}
          debugln(['TMessagesCtrl.SelectNextShown Line=',Line,' Offset=',Offset,' ViewIndex=',i]);
          {$ENDIF}
          repeat
            inc(i);
            if i>=ViewCount then begin
              {$IFDEF VerboseMsgCtrlSelectNextShown}
              debugln(['TMessagesCtrl.SelectNextShown can not go further down']);
              {$ENDIF}
              exit;
            end;
            View:=Views[i];
          until View.HasContent;
          Line:=-1;
        end;
      end else begin
        inc(Line,Offset);
        if Line>=-1 then
          Offset:=0
        else begin
          // previous view
          Offset:=Line+2;
          i:=IndexOfView(View);
          repeat
            dec(i);
            if i<0 then begin
              {$IFDEF VerboseMsgCtrlSelectNextShown}
              debugln(['TMessagesCtrl.SelectNextShown can not go further up']);
              {$ENDIF}
              exit;
            end;
            View:=Views[i];
          until View.HasContent;
          Line:=View.GetShownLineCount(true,true)-1;
        end;
      end;
      {$IFDEF VerboseMsgCtrlSelectNextShown}
      debugln(['TMessagesCtrl.SelectNextShown SELECT Offset=',Offset,' ViewIndex=',IndexOfView(View),' Line=',Line]);
      {$ENDIF}
      Select(View,Line,true,true);
      Result:=true;
    end;
  end;
  {$IFDEF VerboseMsgCtrlSelectNextShown}
  debugln(['TMessagesCtrl.SelectNextShown END ViewIndex=',IndexOfView(SelectedView),' Line=',SelectedLine]);
  {$ENDIF}
end;

function TMessagesCtrl.SelectLast(DoScroll, FullyVisible: boolean): boolean;
var
  i: Integer;
begin
  i:=ViewCount-1;
  while (i>=0) do begin
    if Views[i].HasContent then begin
      Select(Views[i],Views[i].GetShownLineCount(true,true)-1,DoScroll,FullyVisible);
      exit(true);
    end;
    dec(i);
  end;
  Result:=false;
end;

function TMessagesCtrl.SelectFirst(DoScroll, FullyVisible: boolean): boolean;
var
  i: Integer;
begin
  i:=0;
  while (i<ViewCount) do begin
    if Views[i].HasContent then begin
      Select(Views[i],-1,DoScroll,FullyVisible);
      exit(true);
    end;
    inc(i);
  end;
  Result:=false;
end;

function TMessagesCtrl.GetSelectedMsg: TMessageLine;
var
  View: TLMsgWndView;
  Line: Integer;
begin
  Result:=nil;
  View:=SelectedView;
  if View=nil then exit;
  Line:=SelectedLine;
  if (Line<0) then exit;
  if Line<View.Lines.Count then
    Result:=View.Lines[Line]
  else if (Line=View.Lines.Count) and (View.ProgressLine.Msg<>'') then
    Result:=View.ProgressLine;
end;

function TMessagesCtrl.SearchNextUrgent(StartView: TLMsgWndView;
  StartLine: integer; SkipStart, Downwards: boolean;
  aMinUrgency: TMessageLineUrgency; WithSrcPos: boolean; out
  View: TLMsgWndView; out LineNumber: integer): boolean;
var
  CurView: TLMsgWndView;
  CurLine: Integer;
  CurViewLineCnt: integer;
  MsgLine: TMessageLine;

  function Next: boolean;
  var
    i: Integer;
  begin
    if Downwards then begin
      inc(CurLine);
      if CurLine>=CurViewLineCnt then begin
        i:=IndexOfView(CurView);
        repeat
          inc(i);
          if i>=ViewCount then exit(false);
          CurView:=Views[i];
        until CurView.HasContent;
        CurLine:=-1;
        CurViewLineCnt:=CurView.GetShownLineCount(true,true);
      end;
    end else begin
      dec(CurLine);
      if CurLine<-1 then begin
        i:=IndexOfView(CurView);
        repeat
          dec(i);
          if i<0 then exit(false);
          CurView:=Views[i];
        until CurView.HasContent;
        CurViewLineCnt:=CurView.GetShownLineCount(true,true);
        CurLine:=CurViewLineCnt-1;
      end;
    end;
    Result:=true;
  end;

begin
  Result:=false;
  View:=nil;
  LineNumber:=-1;
  if ViewCount=0 then exit;
  if StartView=nil then begin
    // use default start
    if Downwards then begin
      StartView:=Views[0];
      StartLine:=-1;
    end else begin
      StartView:=Views[ViewCount-1];
      StartLine:=StartView.GetShownLineCount(true,true);
    end;
  end;
  CurView:=StartView;
  CurLine:=StartLine;
  CurViewLineCnt:=CurView.GetShownLineCount(true,true);
  // skip invalid line numbers
  if CurLine<-1 then begin
    SkipStart:=false;
    if Downwards then
      CurLine:=-1
    else if not Next then
      exit;
  end else if CurLine>=CurViewLineCnt then begin
    SkipStart:=false;
    if Downwards then begin
      if not Next then exit;
    end else
      CurLine:=CurViewLineCnt-1;
  end;
  // skip invalid views
  if not CurView.HasContent then begin
    SkipStart:=false;
    if not Next then exit;
  end;
  // skip start
  if SkipStart then
    if not Next then exit;
  // search
  repeat
    if (CurLine>=0) and (CurLine<CurView.Lines.Count) then begin
      MsgLine:=CurView.Lines[CurLine];
      if MsgLine.Urgency>=aMinUrgency then begin
        if (not WithSrcPos) or MsgLine.HasSourcePosition then begin
          View:=CurView;
          LineNumber:=CurLine;
          exit(true);
        end;
      end;
    end;
  until not Next;
end;

function TMessagesCtrl.SelectFirstUrgentMessage(
  aMinUrgency: TMessageLineUrgency; WithSrcPos: boolean): boolean;
var
  View: TLMsgWndView;
  LineNumber: integer;
begin
  Result:=false;
  if ViewCount=0 then exit;
  if not SearchNextUrgent(nil,0,false,true,aMinUrgency,WithSrcPos,View,LineNumber)
  then exit;
  Select(View,LineNumber,true,true);
  Result:=true;
end;

function TMessagesCtrl.SelectNextUrgentMessage(
  aMinUrgency: TMessageLineUrgency; WithSrcPos: boolean; Downwards: boolean
  ): boolean;
var
  View: TLMsgWndView;
  LineNumber: integer;
begin
  Result:=false;
  if not SearchNextUrgent(SelectedView,SelectedLine,true,Downwards,
    aMinUrgency,WithSrcPos,View,LineNumber)
  then exit;
  Select(View,LineNumber,true,true);
  Result:=true;
end;

function TMessagesCtrl.IsLineVisible(View: TLMsgWndView; LineNumber: integer
  ): boolean;
var
  y: Integer;
begin
  Result:=false;
  if View=nil then exit;
  y:=GetLineTop(View,LineNumber,true);
  if (y+ItemHeight>0) and (y<ClientHeight) then
    Result:=true;
end;

function TMessagesCtrl.IsLastLineVisible(View: TLMsgWndView): boolean;
var
  LineNumber: Integer;
begin
  LineNumber:=View.GetShownLineCount(false,true)-1;
  Result:=IsLineVisible(View,LineNumber);
end;

function TMessagesCtrl.GetLineText(Line: TMessageLine): string;
begin
  // 'filename(line,column) '
  case FilenameStyle of
  mwfsShort: Result:=Line.GetShortFilename;
  mwfsRelative: Result:=Line.GetRelativeFilename
  else Result:=Line.GetFullFilename;
  end;
  if Line.Line>0 then begin
    Result+='('+IntToStr(Line.Line)+','+IntToStr(Line.Column)+')';
  end;
  if Result<>'' then
    Result+=' ';

  // 'error: '
  if Line.Urgency<>mluImportant then
    Result+=UrgencyToStr(Line.Urgency)+': ';

  // message id
  if (mcoShowMessageID in Options) and (Line.MsgID<>0) then
   Result+='('+IntToStr(Line.MsgID)+') ';

  // message
  if (mcoShowTranslated in Options) and (Line.TranslatedMsg<>'') then
    Result+=Line.TranslatedMsg
  else
    Result+=Line.Msg;
end;

function TMessagesCtrl.GetHeaderText(View: TLMsgWndView): string;

  function GetStats(Lines: TMessageLines): string;
  var
    ErrCnt: Integer;
    WarnCnt: Integer;
    HintCnt: Integer;
    c: TMessageLineUrgency;
  begin
    Result:='';
    ErrCnt:=0;
    WarnCnt:=0;
    HintCnt:=0;
    for c:=Low(Lines.UrgencyCounts) to high(Lines.UrgencyCounts) do begin
      //debugln(['GetStats cat=',dbgs(c),' count=',Lines.UrgencyCounts[c]]);
      if c>=mluError then
        inc(ErrCnt,Lines.UrgencyCounts[c])
      else if c=mluWarning then
        inc(WarnCnt,Lines.UrgencyCounts[c])
      else if c in [mluHint,mluNote] then
        inc(HintCnt,Lines.UrgencyCounts[c]);
    end;
    if ErrCnt>0 then
      Result+=Format(lisErrors2, [IntToStr(ErrCnt)]);
    if WarnCnt>0 then
      Result+=Format(lisWarnings, [IntToStr(WarnCnt)]);
    if HintCnt>0 then
      Result+=Format(lisHints, [IntToStr(HintCnt)]);
  end;

begin
  Result:=View.Caption;
  if Result='' then
    Result:=lisMenuViewMessages;
  if View.SummaryMsg<>'' then
    Result+=': '+View.SummaryMsg;
  if mcoShowStats in Options then begin
    Result+=GetStats(View.Lines);
  end;
end;

function TMessagesCtrl.FindUnfinishedView: TLMsgWndView;
var
  i: Integer;
begin
  for i:=0 to ViewCount-1 do begin
    Result:=Views[i];
    //debugln(['TMessagesCtrl.FindUnfinishedView ',i,' ',ViewCount,' caption="',Result.Caption,'" Result.Tool=',dbgsname(Result.Tool)]);
    if not Result.HasFinished then exit;
  end;
  Result:=nil;
end;

function TMessagesCtrl.GetLastViewWithContent: TLMsgWndView;
var
  i: Integer;
begin
  i:=ViewCount-1;
  while i>=0 do begin
    Result:=Views[i];
    if Result.HasContent then exit;
    dec(i);
  end;
  Result:=nil;
end;

procedure TMessagesCtrl.Select(View: TLMsgWndView; LineNumber: integer;
  DoScroll, FullyVisible: boolean);
begin
  BeginUpdate;
  SelectedView:=View;
  SelectedLine:=LineNumber;
  if DoScroll then
    ScrollToLine(SelectedView,SelectedLine,FullyVisible);
  EndUpdate;
end;

procedure TMessagesCtrl.ScrollToLine(View: TLMsgWndView; LineNumber: integer;
  FullyVisible: boolean);
var
  y: Integer;
  MinScrollTop: integer;
  MaxScrollTop: Integer;
begin
  y:=GetLineTop(View,LineNumber,false);
  if FullyVisible then begin
    MinScrollTop:=Max(0,y+ItemHeight-ClientHeight);
    MaxScrollTop:=y;
  end else begin
    MinScrollTop:=Max(0,y-1-ClientHeight);
    MaxScrollTop:=y+ItemHeight-1;
  end;
  //debugln(['TMessagesCtrl.ScrollToLine ',LineNumber,' y=',y,' Min=',MinScrollTop,' Max=',MaxScrollTop]);
  y:=Max(Min(ScrollTop,MaxScrollTop),MinScrollTop);
  //debugln(['TMessagesCtrl.ScrollToLine y=',y,' ScrollTopMax=',ScrollTopMax]);
  ScrollTop:=y;
end;

function TMessagesCtrl.GetLineTop(View: TLMsgWndView; LineNumber: integer;
  Scrolled: boolean): integer;
var
  i: Integer;
  CurView: TLMsgWndView;
begin
  Result:=0;
  if View=nil then exit;
  for i:=0 to ViewCount-1 do begin
    CurView:=Views[i];
    if CurView=View then break;
    inc(Result,ItemHeight*CurView.GetShownLineCount(true,true));
  end;
  if LineNumber<0 then begin
    // header
  end else if LineNumber<View.Lines.Count then begin
    // normal messages
    inc(Result,(LineNumber+1)*ItemHeight);
  end else begin
    // last line
    inc(Result,(View.Lines.Count+1)*ItemHeight);
  end;
  if Scrolled then
    dec(Result,ScrollTop);
end;

constructor TMessagesCtrl.Create(AOwner: TComponent);
var
  u: TMessageLineUrgency;
begin
  inherited Create(AOwner);
  ControlStyle:=ControlStyle-[csCaptureMouse]+[csReflector];
  FOptions:=MCDefaultOptions;
  Filters.OnChanged:=@OnFilterChanged;
  FActiveFilter:=Filters[0];
  FViews:=TFPList.Create;
  FUpdateTimer:=TTimer.Create(Self);
  FUpdateTimer.Name:='MsgUpdateTimer';
  FUpdateTimer.Interval:=200;
  FUpdateTimer.OnTimer:=@MsgUpdateTimerTimer;
  FItemHeight:=20;
  FSelectedView:=nil;
  FSelectedLine:=-1;
  BorderWidth:=0;
  fBackgroundColor:=MsgWndDefBackgroundColor;
  FHeaderBackground[lmvtsRunning]:=MsgWndDefHeaderBackgroundRunning;
  FHeaderBackground[lmvtsSuccess]:=MsgWndDefHeaderBackgroundSuccess;
  FHeaderBackground[lmvtsFailed]:=MsgWndDefHeaderBackgroundFailed;
  FAutoHeaderBackground:=MsgWndDefAutoHeaderBackground;
  FTextColor:=MsgWndDefTextColor;
  TabStop := True;
  ParentColor := False;
  FImageChangeLink := TChangeLink.Create;
  FImageChangeLink.OnChange := @ImageListChange;
  for u:=Low(TMessageLineUrgency) to high(TMessageLineUrgency) do
    fUrgencyStyles[u]:=TMsgCtrlUrgencyStyle.Create(Self,u);
  ShowHint:=true;
end;

destructor TMessagesCtrl.Destroy;
var
  u: TMessageLineUrgency;
begin
  IdleConnected:=false;
  Images:=nil;
  ClearViews(false);

  FreeAndNil(FViews);
  FreeAndNil(FUpdateTimer);
  FreeAndNil(FImageChangeLink);
  for u:=Low(TMessageLineUrgency) to high(TMessageLineUrgency) do
    FreeAndNil(fUrgencyStyles[u]);
  inherited Destroy;
end;

procedure TMessagesCtrl.BeginUpdate;
begin
  inc(fUpdateLock);
end;

procedure TMessagesCtrl.EndUpdate;
begin
  if fUpdateLock=0 then
    raise Exception.Create('');
  dec(fUpdateLock);
end;

procedure TMessagesCtrl.EraseBackground(DC: HDC);
begin
  // everything is painted, so erasing the background is not needed
end;

procedure TMessagesCtrl.ApplyEnvironmentOptions;
var
  NewOptions: TMsgCtrlOptions;
  u: TMessageLineUrgency;

  procedure SetOption(Option: TMsgCtrlOption; State: boolean);
  begin
    if State then
      NewOptions:=NewOptions+[Option]
    else
      NewOptions:=NewOptions-[Option];
  end;

begin
  for u in TMessageLineUrgency do
    UrgencyStyles[u].Color := EnvironmentOptions.MsgColors[u];
  BackgroundColor:=EnvironmentOptions.MsgViewColors[mwBackground];
  AutoHeaderBackground:=EnvironmentOptions.MsgViewColors[mwAutoHeader];
  HeaderBackground[lmvtsRunning]:=EnvironmentOptions.MsgViewColors[mwRunning];
  HeaderBackground[lmvtsSuccess]:=EnvironmentOptions.MsgViewColors[mwSuccess];
  HeaderBackground[lmvtsFailed]:=EnvironmentOptions.MsgViewColors[mwFailed];
  TextColor:=EnvironmentOptions.MsgViewColors[mwTextColor];
  NewOptions:=Options;
  SetOption(mcoSingleClickOpensFile,not EnvironmentOptions.MsgViewDblClickJumps);
  SetOption(mcoShowMsgIcons,EnvironmentOptions.ShowMessagesIcons);
  SetOption(mcoShowTranslated,EnvironmentOptions.MsgViewShowTranslations);
  SetOption(mcoAlwaysDrawFocused,EnvironmentOptions.MsgViewAlwaysDrawFocused);
  Options:=NewOptions;
  FilenameStyle:=EnvironmentOptions.MsgViewFilenameStyle;
end;

function TMessagesCtrl.IndexOfView(View: TLMsgWndView): integer;
begin
  Result:=FViews.IndexOf(View);
end;

procedure TMessagesCtrl.ClearViews(OnlyFinished: boolean);
var
  i: Integer;
  View: TLMsgWndView;
begin
  if OnlyFinished then begin
    for i:=ViewCount-1 downto 0 do begin
      if i>=ViewCount then continue;
      View:=Views[i];
      if View.HasFinished then
        View.Free;
    end;
  end else begin
    while ViewCount>0 do
      Views[0].Free;
  end;
end;

procedure TMessagesCtrl.RemoveView(View: TLMsgWndView);
begin
  if FViews.IndexOf(View)<0 then exit;
  FViews.Remove(View);
  View.FControl:=nil;
  View.OnChanged:=nil;
  if fLastSearchStartView=View then
    fLastSearchStartView:=nil;
  if SelectedView=View then
    SelectedView:=nil;
  UpdateScrollBar(true);
  Invalidate;
end;

function TMessagesCtrl.GetView(aCaption: string; CreateIfNotExist: boolean
  ): TLMsgWndView;
var
  i: Integer;
begin
  for i:=0 to ViewCount-1 do begin
    Result:=Views[i];
    if UTF8CompareStr(aCaption,Result.Caption)=0 then exit;
  end;
  if not CreateIfNotExist then
    exit(nil);
  Result:=TLMsgWndView.Create(Self);
  Result.FControl:=Self;
  Result.Caption:=aCaption;
  Result.Filter.Assign(ActiveFilter);
  FViews.Add(Result);
  FreeNotification(Result);
  Result.OnChanged:=@OnViewChanged;
  fSomeViewsRunning:=true;
end;

function TMessagesCtrl.GetLineAt(Y: integer; out View: TLMsgWndView; out
  Line: integer): boolean;
var
  i: Integer;
begin
  for i:=0 to ViewCount-1 do begin
    View:=Views[i];
    if View.FPaintStamp<>FPaintStamp then continue;
    if (View.fPaintTop>Y) or (View.fPaintBottom<Y) then continue;
    Line:=((Y-View.fPaintTop) div ItemHeight)-1;
    Result:=true;
    exit;
  end;
  View:=nil;
  Line:=-1;
  Result:=false;
end;

function TMessagesCtrl.ScrollLeftMax: integer;
begin
  Result:=0;
end;

function TMessagesCtrl.ScrollTopMax: integer;
var
  i: Integer;
  View: TLMsgWndView;
begin
  if fScrollTopMax<0 then begin
    fScrollTopMax:=0;
    for i:=0 to ViewCount-1 do begin
      View:=Views[i];
      inc(fScrollTopMax,View.GetShownLineCount(true,true)*ItemHeight);
    end;
    fScrollTopMax:=Max(0,fScrollTopMax-ClientHeight);
  end;
  Result:=fScrollTopMax;
end;

procedure TMessagesCtrl.StoreSelectedAsSearchStart;
begin
  fLastLoSearchText:=UTF8LowerCase(FSearchText);
  fLastSearchStartView:=FSelectedView;
  fLastSearchStartLine:=FSelectedLine;
end;

function TMessagesCtrl.OpenSelection: boolean;
var
  Msg: TMessageLine;
begin
  Result:=false;
  if not Assigned(OnOpenMessage) then exit;
  Msg:=GetSelectedMsg;
  if Msg=nil then exit;
  Result:=OnOpenMessage(Self,Msg);
end;

procedure TMessagesCtrl.CreateMarksForFile(aSynEdit: TSynEdit;
  aFilename: string; DeleteOld: boolean);
var
  i: Integer;
  Lines: TMessageLines;
  Line: TMessageLine;
begin
  if DeleteOld then
    SourceMarks.RemoveMarks(aSynEdit);
  for i:=0 to ViewCount-1 do begin
    Lines:=Views[i].Lines;
    for Line in Lines.EnumerateFile(aFilename,0,High(Integer)) do begin
      //debugln(['TMessagesCtrl.CreateMarksForFile ',GetLineText(Line)]);
      CreateSourceMark(Line,aSynEdit);
    end;
  end;
end;

function TMessagesCtrl.ApplySrcChanges(Changes: TETSingleSrcChanges): boolean;
var
  i: Integer;
begin
  Result:=false;
  //debugln(['TMessagesCtrl.ApplySrcChanges ViewCount=',ViewCount]);
  for i:=0 to ViewCount-1 do
    if Views[i].ApplySrcChanges(Changes) then
      Result:=true;
  if Result then
    Invalidate;
end;

function TMessagesCtrl.HasSelection: boolean;
var
  View: TLMsgWndView;
begin
  Result:=false;
  View:=SelectedView;
  if View=nil then exit;
  Result:=SelectedLine<View.GetShownLineCount(false,true);
end;

function TMessagesCtrl.IsLineSelected(View: TLMsgWndView; LineNumber: integer
  ): boolean;
begin
  Result:=(View=SelectedView) and (LineNumber=SelectedLine);
end;

{ TMessagesFrame }

procedure TMessagesFrame.MsgCtrlPopupMenuPopup(Sender: TObject);

  procedure UpdateRemoveCompOptHideMsgItems;
  var
    i: Integer;
    View: TLMsgWndView;
    ToolData: TIDEExternalToolData;
    IDETool: TObject;
    CompOpts: TBaseCompilerOptions;
    Flag: PCompilerMsgIdFlag;
    Pattern: String;
    Pkg: TIDEPackage;
    Cnt: Integer;
    Item: TIDEMenuCommand;
    ModuleName: String;
  begin
    // create one menuitem per compiler option
    Cnt:=0;
    for i:=0 to ViewCount-1 do begin
      View:=Views[i];
      if View.Tool=nil then continue;
      ToolData:=TIDEExternalToolData(View.Tool.Data);
      if not (ToolData is TIDEExternalToolData) then continue;
      IDETool:=ExternalTools.GetIDEObject(ToolData);
      if IDETool=nil then continue;
      if IDETool is TLazProject then begin
        CompOpts:=TLazProject(IDETool).LazCompilerOptions as TBaseCompilerOptions;
        ModuleName:=lisProjectOption;
      end else if IDETool is TIDEPackage then begin
        Pkg:=TIDEPackage(IDETool);
        CompOpts:=Pkg.LazCompilerOptions as TBaseCompilerOptions;
        ModuleName:=Format(lisPackageOption, [Pkg.Name]);
      end else
        continue;
      for Flag in CompOpts.IDEMessageFlags do begin
        if Flag^.Flag<>cfvHide then continue;
        if Cnt>=MsgRemoveCompOptHideMenuSection.Count then begin
          Item:=RegisterIDEMenuCommand(MsgRemoveCompOptHideMenuSection,'RemoveCompOptHideMsg'+IntToStr(Cnt),'');
          Item.OnClick:=@RemoveCompOptHideMsgClick;
        end else begin
          Item:=MsgRemoveCompOptHideMenuSection.Items[Cnt] as TIDEMenuCommand;
        end;
        Item.Tag:=Flag^.MsgId;
        Item.UserTag:=PtrUInt(ToolData);
        Pattern:=GetMsgPattern(SubToolFPC,Flag^.MsgID,true,40);
        Item.Caption:=ModuleName+': '+Pattern;
        inc(Cnt);
      end;
    end;
    MsgRemoveCompOptHideMenuSection.Visible:=Cnt>0;
    // delete old menu items
    while MsgRemoveCompOptHideMenuSection.Count>Cnt do
      MsgRemoveCompOptHideMenuSection[Cnt].Free;
  end;

  procedure UpdateRemoveMsgTypeFilterItems;
  var
    FilterItem: TLMVFilterMsgType;
    i: Integer;
    Item: TIDEMenuCommand;
    Cnt: Integer;
  begin
    // create one menuitem per filter item
    Cnt:=MessagesCtrl.ActiveFilter.FilterMsgTypeCount;
    MsgRemoveMsgTypeFilterMenuSection.Visible:=Cnt>0;
    for i:=0 to Cnt-1 do begin
      if i>=MsgRemoveFilterMsgOneTypeMenuSection.Count then begin
        Item:=RegisterIDEMenuCommand(MsgRemoveFilterMsgOneTypeMenuSection,'MsgRemoveMsgOfTypeFilter'+IntToStr(i),'');
        Item.Tag:=i;
        Item.OnClick:=@RemoveFilterMsgTypeClick;
      end else
        Item:=MsgRemoveFilterMsgOneTypeMenuSection.Items[i] as TIDEMenuCommand;
      FilterItem:=MessagesCtrl.ActiveFilter.FilterMsgTypes[i];
      Item.Caption:=GetMsgPattern(FilterItem.SubTool,FilterItem.MsgID,true,40);
    end;
    // delete old menu items
    while MsgRemoveFilterMsgOneTypeMenuSection.Count>Cnt do
      MsgRemoveFilterMsgOneTypeMenuSection[Cnt].Free;
    MsgRemoveFilterAllMsgTypesMenuItem.OnClick:=@ClearFilterMsgTypesMenuItemClick;
  end;

  procedure UpdateFilterItems;
  var
    i: Integer;
    Filter: TLMsgViewFilter;
    Item: TIDEMenuCommand;
    Cnt: Integer;
  begin
    Cnt:=MessagesCtrl.Filters.Count;
    for i:=0 to Cnt-1 do begin
      Filter:=MessagesCtrl.Filters[i];
      if i>=MsgSelectFilterMenuSection.Count then begin
        Item:=RegisterIDEMenuCommand(MsgSelectFilterMenuSection,'MsgSelectFilter'+IntToStr(i),'');
        Item.Tag:=i;
        Item.OnClick:=@OnSelectFilterClick;
      end else
        Item:=MsgSelectFilterMenuSection[i] as TIDEMenuCommand;
      Item.Caption:=Filter.Caption;
      Item.Checked:=Filter=MessagesCtrl.ActiveFilter;
    end;
    // delete old menu items
    while MsgSelectFilterMenuSection.Count>Cnt do
      MsgSelectFilterMenuSection[Cnt].Free;

    MsgAddFilterMenuItem.OnClick:=@AddFilterMenuItemClick;
  end;

  procedure UpdateQuickFixes(CurLine: TMessageLine);
  begin
    // delete old
    MsgQuickFixMenuSection.Clear;
    // create items
    if CurLine<>nil then begin
      IDEQuickFixes.SetMsgLines(CurLine);
      IDEQuickFixes.OnPopupMenu(MsgQuickFixMenuSection);
    end;
    MsgQuickFixMenuSection.Visible:=MsgQuickFixMenuSection.Count>0;
  end;

var
  HasText: Boolean;
  View: TLMsgWndView;
  HasFilename: Boolean;
  LineNumber: Integer;
  Line: TMessageLine;
  i: Integer;
  HasViewContent: Boolean;
  Running: Boolean;
  MsgType: String;
  CanFilterMsgType: Boolean;
  MinUrgency: TMessageLineUrgency;
  ToolData: TIDEExternalToolData;
  ToolOptionsCaption: String;
  VisibleCnt: Integer;
begin
  MessagesMenuRoot.MenuItem:=MsgCtrlPopupMenu.Items;
  MessagesMenuRoot.BeginUpdate;
  try
    HasText:=false;
    HasFilename:=false;
    MsgType:='';
    CanFilterMsgType:=false;
    Line:=nil;
    HasViewContent:=false;
    Running:=false;

    // check all
    for i:=0 to MessagesCtrl.ViewCount-1 do begin
      View:=MessagesCtrl.Views[i];
      if View.HasContent then
        HasViewContent:=true;
      if View.Running then
        Running:=true;
    end;

    MsgFindMenuItem.OnClick:=@FindMenuItemClick;

    // check selection
    View:=MessagesCtrl.SelectedView;
    if View<>nil then begin
      LineNumber:=MessagesCtrl.SelectedLine;
      if (LineNumber>=0) and (LineNumber<View.Lines.Count) then begin
        Line:=View.Lines[LineNumber];
        HasFilename:=Line.Filename<>'';
        HasText:=Line.Msg<>'';
        if (Line.SubTool<>'') and (Line.MsgID<>0) then begin
          MsgType:=GetMsgPattern(Line.SubTool,Line.MsgID,true,40);
          CanFilterMsgType:=ord(Line.Urgency)<ord(mluError);
        end;
      end;
    end else begin
      // no line selected => use last visible View
      View:=MessagesCtrl.GetLastViewWithContent;
    end;
    ToolOptionsCaption:='';

    // About
    if View<>nil then
    begin
      MsgAboutToolMenuItem.Caption:=Format(lisAbout2, [View.Caption]);
      MsgAboutSection.Visible:=true;
      if (View.Tool<>nil) and (View.Tool.Data is TIDEExternalToolData) then begin
        ToolData:=TIDEExternalToolData(View.Tool.Data);
        if ToolData.Kind=IDEToolCompilePackage then
          ToolOptionsCaption:=Format(lisCPOpenPackage, [ToolData.ModuleName]);
      end;
    end else
      MsgAboutSection.Visible:=false;
    MsgAboutToolMenuItem.OnClick:=@AboutToolMenuItemClick;
    VisibleCnt:=1;
    MsgOpenToolOptionsMenuItem.Visible:=ToolOptionsCaption<>'';
    if MsgOpenToolOptionsMenuItem.Visible then
    begin
      inc(VisibleCnt);
      //only assign caption if it is not empty to avoid its "unlocalizing",
      //this is visible e.g. in EditorToolBar menu tree
      MsgOpenToolOptionsMenuItem.Caption:=ToolOptionsCaption;
    end
    else
      //assign default caption if item is not visible (needed for EditorToolBar)
      MsgOpenToolOptionsMenuItem.Caption:=lisOpenToolOptions;
    MsgOpenToolOptionsMenuItem.OnClick:=@OpenToolsOptionsMenuItemClick;
    MsgAboutSection.ChildsAsSubMenu:=VisibleCnt>1;

    // Filtering
    if CanFilterMsgType then begin
      MsgFilterMsgOfTypeMenuItem.Caption:=Format(lisFilterAllMessagesOfType, [MsgType]);
      MsgFilterMsgOfTypeMenuItem.Visible:=true;
    end else begin
      //assign default caption if item is not visible (needed for EditorToolBar)
      MsgFilterMsgOfTypeMenuItem.Caption:=lisFilterAllMessagesOfCertainType;
      MsgFilterMsgOfTypeMenuItem.Visible:=false;
    end;
    MsgFilterMsgOfTypeMenuItem.OnClick:=@FilterMsgOfTypeMenuItemClick;
    MsgFilterHintsWithoutPosMenuItem.Checked:=MessagesCtrl.ActiveFilter.FilterNotesWithoutPos;
    MsgFilterHintsWithoutPosMenuItem.OnClick:=@FilterHintsWithoutPosMenuItemClick;

    MinUrgency:=MessagesCtrl.ActiveFilter.MinUrgency;
    MsgFilterNoneMenuItem.Checked:=MinUrgency in [mluNone..mluDebug];
    MsgFilterNoneMenuItem.OnClick:=@FilterUrgencyMenuItemClick;
    MsgFilterDebugMenuItem.Checked:=MinUrgency in [mluVerbose3..mluVerbose];
    MsgFilterDebugMenuItem.OnClick:=@FilterUrgencyMenuItemClick;
    MsgFilterVerboseMenuItem.Checked:=MinUrgency=mluHint;
    MsgFilterVerboseMenuItem.OnClick:=@FilterUrgencyMenuItemClick;
    MsgFilterHintsMenuItem.Checked:=MinUrgency=mluNote;
    MsgFilterHintsMenuItem.OnClick:=@FilterUrgencyMenuItemClick;
    MsgFilterNotesMenuItem.Checked:=MinUrgency in [mluWarning..mluImportant];
    MsgFilterNotesMenuItem.OnClick:=@FilterUrgencyMenuItemClick;
    MsgFilterWarningsMenuItem.Checked:=MinUrgency>=mluError;
    MsgFilterWarningsMenuItem.OnClick:=@FilterUrgencyMenuItemClick;

    // Copying
    MsgCopyMsgMenuItem.Enabled:=HasText;
    MsgCopyMsgMenuItem.OnClick:=@CopyMsgMenuItemClick;
    MsgCopyFilenameMenuItem.Enabled:=HasFilename;
    MsgCopyFilenameMenuItem.OnClick:=@CopyFilenameMenuItemClick;
    MsgCopyAllMenuItem.Enabled:=not Running;
    MsgCopyAllMenuItem.OnClick:=@CopyAllMenuItemClick;
    MsgCopyShownMenuItem.Enabled:=HasViewContent;
    MsgCopyShownMenuItem.OnClick:=@CopyShownMenuItemClick;

    // Saving
    MsgSaveAllToFileMenuItem.Enabled:=not Running;
    MsgSaveAllToFileMenuItem.OnClick:=@SaveAllToFileMenuItemClick;
    MsgSaveShownToFileMenuItem.Enabled:=HasViewContent;
    MsgSaveShownToFileMenuItem.OnClick:=@SaveShownToFileMenuItemClick;
    MsgHelpMenuItem.Enabled:=HasText;
    MsgHelpMenuItem.OnClick:=@HelpMenuItemClick;
    MsgEditHelpMenuItem.OnClick:=@EditHelpMenuItemClick;
    MsgClearMenuItem.OnClick:=@ClearMenuItemClick;
    MsgClearMenuItem.Enabled:=View<>nil;

    // Options
    MsgWndStayOnTopMenuItem.Checked:=mcoWndStayOnTop in MessagesCtrl.Options;
    MsgWndStayOnTopMenuItem.OnClick:=@WndStayOnTopMenuItemClick;
    MsgFileStyleShortMenuItem.Checked:=MessagesCtrl.FilenameStyle=mwfsShort;
    MsgFileStyleShortMenuItem.OnClick:=@FileStyleMenuItemClick;
    MsgFileStyleRelativeMenuItem.Checked:=MessagesCtrl.FilenameStyle=mwfsRelative;
    MsgFileStyleRelativeMenuItem.OnClick:=@FileStyleMenuItemClick;
    MsgFileStyleFullMenuItem.Checked:=MessagesCtrl.FilenameStyle=mwfsFull;
    MsgFileStyleFullMenuItem.OnClick:=@FileStyleMenuItemClick;

    MsgTranslateMenuItem.Checked:=mcoShowTranslated in MessagesCtrl.Options;
    MsgTranslateMenuItem.OnClick:=@TranslateMenuItemClick;
    MsgShowIDMenuItem.Checked:=mcoShowMessageID in MessagesCtrl.Options;
    MsgShowIDMenuItem.OnClick:=@ShowIDMenuItemClick;
    MsgMoreOptionsMenuItem.OnClick:=@MoreOptionsMenuItemClick;

    UpdateRemoveCompOptHideMsgItems;
    UpdateRemoveMsgTypeFilterItems;
    UpdateFilterItems;

    UpdateQuickFixes(Line);
  finally
    MessagesMenuRoot.EndUpdate;
  end;
end;

procedure TMessagesFrame.OnSelectFilterClick(Sender: TObject);
var
  Filter: TLMsgViewFilter;
  Item: TIDEMenuCommand;
begin
  Item:=Sender as TIDEMenuCommand;
  Filter:=MessagesCtrl.Filters.GetFilter(Item.Caption,false);
  if Filter=nil then exit;
  MessagesCtrl.ActiveFilter:=Filter;
end;

procedure TMessagesFrame.OpenToolsOptionsMenuItemClick(Sender: TObject);
var
  View: TLMsgWndView;
  ToolData: TIDEExternalToolData;
begin
  View:=GetAboutView;
  if (View=nil) or (View.Tool=nil) then exit;
  ToolData:=TIDEExternalToolData(View.Tool.Data);
  if not (ToolData is TIDEExternalToolData) then exit;
  if ToolData.Kind=IDEToolCompilePackage then begin
    PackageEditingInterface.DoOpenPackageFile(ToolData.Filename,
                                              [pofAddToRecent],false);
  end;
end;

procedure TMessagesFrame.RemoveCompOptHideMsgClick(Sender: TObject);
var
  Item: TIDEMenuCommand;
  MsgId: Integer;
  ToolData: TIDEExternalToolData;
  IDETool: TObject;
  CompOpts: TLazCompilerOptions;
  Pkg: TIDEPackage;
begin
  if not (Sender is TIDEMenuCommand) then exit;
  Item:=TIDEMenuCommand(Sender);
  MsgId:=Item.Tag;
  ToolData:=TIDEExternalToolData(Item.UserTag);
  IDETool:=ExternalTools.GetIDEObject(ToolData);
  if IDETool=nil then exit;
  if IDETool is TLazProject then begin
    CompOpts:=TLazProject(IDETool).LazCompilerOptions;
    CompOpts.MessageFlags[MsgID]:=cfvNone;
  end else if IDETool is TIDEPackage then begin
    if PackageEditingInterface.DoOpenPackageFile(ToolData.Filename,
                                        [pofAddToRecent],false)<>mrOk then exit;
    Pkg:=PackageEditingInterface.FindPackageWithName(ToolData.ModuleName);
    if Pkg=nil then exit;
    CompOpts:=Pkg.LazCompilerOptions;
    CompOpts.MessageFlags[MsgID]:=cfvNone;
  end;
end;

procedure TMessagesFrame.SaveAllToFileMenuItemClick(Sender: TObject);
begin
  SaveClicked(false);
end;

procedure TMessagesFrame.SaveShownToFileMenuItemClick(Sender: TObject);
begin
  SaveClicked(true);
end;

procedure TMessagesFrame.SearchEditChange(Sender: TObject);
var
  s: TCaption;
begin
  s:=SearchEdit.Text;
  MessagesCtrl.SearchText:=s;
end;

procedure TMessagesFrame.SearchEditKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key=VK_ESCAPE) then
    HideSearch;
end;

procedure TMessagesFrame.SearchNextSpeedButtonClick(Sender: TObject);
begin
  MessagesCtrl.SelectNextOccurence(true);
end;

procedure TMessagesFrame.SearchPrevSpeedButtonClick(Sender: TObject);
begin
  MessagesCtrl.SelectNextOccurence(false);
end;

procedure TMessagesFrame.ShowIDMenuItemClick(Sender: TObject);
begin
  if mcoShowMessageID in MessagesCtrl.Options then
    MessagesCtrl.Options:=MessagesCtrl.Options-[mcoShowMessageID]
  else
    MessagesCtrl.Options:=MessagesCtrl.Options+[mcoShowMessageID];
end;

procedure TMessagesFrame.SrcEditLinesChanged(Sender: TObject);
begin
  //debugln(['TMessagesFrame.SrcEditLinesChanged ',DbgSName(Sender)]);
  if Sender is TETSynPlugin then
    ApplySrcChanges(TETSynPlugin(Sender).Changes);
end;

procedure TMessagesFrame.TranslateMenuItemClick(Sender: TObject);
begin
  if mcoShowTranslated in MessagesCtrl.Options then
    MessagesCtrl.Options:=MessagesCtrl.Options-[mcoShowTranslated]
  else
    MessagesCtrl.Options:=MessagesCtrl.Options+[mcoShowTranslated];
  EnvironmentOptions.MsgViewShowTranslations:=mcoShowTranslated in MessagesCtrl.Options;
end;

procedure TMessagesFrame.RemoveFilterMsgTypeClick(Sender: TObject);
var
  i: PtrInt;
begin
  i:=TIDEMenuCommand(Sender).Tag;
  if i<MessagesCtrl.ActiveFilter.FilterMsgTypeCount then
    MessagesCtrl.ActiveFilter.DeleteFilterMsgType(i);
end;

procedure TMessagesFrame.WndStayOnTopMenuItemClick(Sender: TObject);
begin
  if mcoWndStayOnTop in MessagesCtrl.Options then
    MessagesCtrl.Options:=MessagesCtrl.Options-[mcoWndStayOnTop]
  else
    MessagesCtrl.Options:=MessagesCtrl.Options+[mcoWndStayOnTop];
  EnvironmentOptions.MsgViewStayOnTop:=mcoWndStayOnTop in MessagesCtrl.Options;
end;

function TMessagesFrame.AllMessagesAsString(const OnlyShown: boolean): String;
var
  s: String;
  Tool: TAbstractExternalTool;
  View: TLMsgWndView;
  j: Integer;
  i: Integer;
begin
  s:='';
  for i:=0 to MessagesCtrl.ViewCount-1 do begin
    View:=MessagesCtrl.Views[i];
    if OnlyShown or (View.Tool=nil) then begin
      // save shown messages
      if not View.HasContent then continue;
      s+=MessagesCtrl.GetHeaderText(View)+LineEnding;
      for j:=0 to View.Lines.Count-1 do
        s+=MessagesCtrl.GetLineText(View.Lines[j])+LineEnding;
    end else begin
      // save raw data
      if View.Running then continue;
      Tool:=View.Tool;
      Tool.EnterCriticalSection;
      try
        for j:=0 to Tool.WorkerOutput.Count-1 do
          s+=Tool.WorkerOutput[j]+LineEnding;
      finally
        Tool.LeaveCriticalSection;
      end;
    end;
  end;
  Result:=s;
end;

function TMessagesFrame.GetAboutView: TLMsgWndView;
begin
  Result:=MessagesCtrl.SelectedView;
  if Result=nil then
    Result:=MessagesCtrl.GetLastViewWithContent;
end;

procedure TMessagesFrame.CopyMsgMenuItemClick(Sender: TObject);
begin
  CopyMsgToClipboard(false);
end;

procedure TMessagesFrame.CopyShownMenuItemClick(Sender: TObject);
begin
  CopyAllClicked(true);
end;

procedure TMessagesFrame.EditHelpMenuItemClick(Sender: TObject);
begin
  ShowMessageHelpEditor;
end;

procedure TMessagesFrame.FileStyleMenuItemClick(Sender: TObject);
begin
  if Sender=MsgFileStyleShortMenuItem then
    MessagesCtrl.FilenameStyle:=mwfsShort
  else if Sender=MsgFileStyleRelativeMenuItem then
    MessagesCtrl.FilenameStyle:=mwfsRelative
  else if Sender=MsgFileStyleFullMenuItem then
    MessagesCtrl.FilenameStyle:=mwfsFull;
  EnvironmentOptions.MsgViewFilenameStyle:=MessagesCtrl.FilenameStyle;
end;

procedure TMessagesFrame.FindMenuItemClick(Sender: TObject);
begin
  MessagesCtrl.StoreSelectedAsSearchStart;
  SearchPanel.Visible:=true;
  SearchEdit.SetFocus;
end;

procedure TMessagesFrame.HelpMenuItemClick(Sender: TObject);
begin
  ExecuteIDECommand(Self, ecContextHelp);
end;

procedure TMessagesFrame.FilterHintsWithoutPosMenuItemClick(Sender: TObject);
begin
  MessagesCtrl.ActiveFilter.FilterNotesWithoutPos:=not MessagesCtrl.ActiveFilter.FilterNotesWithoutPos;
end;

procedure TMessagesFrame.FilterMsgOfTypeMenuItemClick(Sender: TObject);
var
  Line: TMessageLine;
begin
  Line:=MessagesCtrl.GetSelectedMsg;
  if (Line=nil) or (ord(Line.Urgency)>=ord(mluError)) then exit;
  MessagesCtrl.ActiveFilter.AddFilterMsgType(Line.SubTool,Line.MsgID);
end;

procedure TMessagesFrame.FilterUrgencyMenuItemClick(Sender: TObject);
var
  MinUrgency: TMessageLineUrgency;
begin
  //debugln(['TMessagesFrame.FilterUrgencyMenuItemClick ',DbgSName(Sender),' ',(Sender as TIDEMenuCommand).Caption,' ',(Sender as TIDEMenuCommand).Checked]);
  if Sender=MsgFilterWarningsMenuItem then
    MinUrgency:=mluError
  else if Sender=MsgFilterNotesMenuItem then
    MinUrgency:=mluWarning
  else if Sender=MsgFilterHintsMenuItem then
    MinUrgency:=mluNote
  else if Sender=MsgFilterVerboseMenuItem then
    MinUrgency:=mluHint
  else if Sender=MsgFilterDebugMenuItem then
    MinUrgency:=mluVerbose3
  else {if Sender=MsgFilterNoneMenuItem then}
    MinUrgency:=mluNone;
  MessagesCtrl.ActiveFilter.MinUrgency:=MinUrgency;
  //debugln(['TMessagesFrame.FilterUrgencyMenuItemClick ',MessageLineUrgencyNames[MinUrgency]]);
end;

procedure TMessagesFrame.HideSearchSpeedButtonClick(Sender: TObject);
begin
  HideSearch;
end;

procedure TMessagesFrame.MoreOptionsMenuItemClick(Sender: TObject);
begin
  LazarusIDE.DoOpenIDEOptions(TMsgWndOptionsFrame);
end;

procedure TMessagesFrame.CopyFilenameMenuItemClick(Sender: TObject);
begin
  CopyMsgToClipboard(true);
end;

procedure TMessagesFrame.CopyAllMenuItemClick(Sender: TObject);
begin
  CopyAllClicked(false);
end;

procedure TMessagesFrame.AboutToolMenuItemClick(Sender: TObject);
var
  View: TLMsgWndView;
  Form: TForm;
  s: String;
  Tool: TAbstractExternalTool;
  Proc: TProcessUTF8;
  Memo: TMemo;
begin
  View:=GetAboutView;
  if View=nil then exit;
  s:=View.Caption+LineEnding;
  s+=LineEnding;
  Tool:=View.Tool;
  if Tool<>nil then begin
    if Tool.Hint<>'' then
      s+=Tool.Hint+LineEnding+LineEnding;
    Proc:=Tool.Process;
    if Proc<>nil then begin
      if Proc.Executable<>'' then
        s+='Executable: '+LineEnding+Proc.Executable+LineEnding+LineEnding;
      if Proc.CurrentDirectory<>'' then
        s+='CurrentDirectory: '+LineEnding+Proc.CurrentDirectory+LineEnding+LineEnding;
      if Proc.Desktop<>'' then
        s+='Desktop: '+Proc.Desktop+LineEnding;
      if Tool.EnvironmentOverrides.Text<>'' then
        s+='Environment Overrides:'+LineEnding
          +Tool.EnvironmentOverrides.Text+LineEnding;
      s+='Parameters:'+LineEnding;
      s+=Proc.Parameters.Text+LineEnding;
      s+='Command Line:'+LineEnding;
      s+=Tool.Process.Executable+' '+Tool.CmdLineParams+LineEnding+LineEnding;
      s+='ProcessID:'+LineEnding+IntToStr(Proc.ProcessID)+LineEnding+LineEnding;
      if Tool.Terminated then
        s+='Terminated'+LineEnding+LineEnding
      else
        s+='ExitStatus:'+LineEnding+IntToStr(Proc.ExitStatus)+LineEnding+LineEnding;
    end;
    if Tool.ErrorMessage<>'' then
      s+=lisError+Tool.ErrorMessage+LineEnding+LineEnding;
  end;

  Form:=TForm.CreateNew(Self);
  try
    with Form do begin
      Name:='AboutExtToolDlg';
      Width:=500;
      Height:=300;
      Position:=poScreenCenter;
      Caption:=Format(lisAbout2, [View.Caption]);
    end;

    Memo:=TMemo.Create(Form);
    with Memo do begin
      Name:='Memo';
      Lines.Text:=s;
      Align:=alClient;
      WordWrap:=true; // carbon requires this and it is a good idea in general
      ScrollBars:=ssVertical;
      ReadOnly:=true;
      Parent:=Form;
    end;
    Form.ShowModal;
  finally
    Form.Free;
  end;
end;

procedure TMessagesFrame.AddFilterMenuItemClick(Sender: TObject);
var
  aCaption: String;
  i: Integer;
  NewFilter: TLMsgViewFilter;
  Filters: TLMsgViewFilters;
begin
  aCaption:=lisFilter;
  i:=1;
  Filters:=MessagesCtrl.Filters;
  while Filters.GetFilter(aCaption+IntToStr(i),false)<>nil do
    inc(i);
  if not InputQuery(lisCreateFilter, lisCodeToolsDefsName, aCaption) then exit;
  aCaption:=UTF8Trim(aCaption,[]);
  if aCaption='' then exit;
  if Filters.GetFilter(aCaption,false)<>nil then begin
    IDEMessageDialog(lisFilterAlreadyExists, Format(
      lisAFilterWithTheNameAlreadyExists, [aCaption]), mtError, [mbCancel], '');
    exit;
  end;
  NewFilter:=Filters.GetFilter(aCaption,true);
  NewFilter.Assign(MessagesCtrl.ActiveFilter);
  MessagesCtrl.ActiveFilter:=NewFilter;
end;

procedure TMessagesFrame.ClearFilterMsgTypesMenuItemClick(Sender: TObject);
begin
  MessagesCtrl.ActiveFilter.ClearFilterMsgTypes;
end;

procedure TMessagesFrame.ClearMenuItemClick(Sender: TObject);
begin
  MessagesCtrl.ClearViews(true);
end;

function TMessagesFrame.GetViews(Index: integer): TLMsgWndView;
begin
  Result:=MessagesCtrl.Views[Index];
end;

procedure TMessagesFrame.HideSearch;
begin
  SearchPanel.Visible:=false;
  MessagesCtrl.SearchText:='';
end;

procedure TMessagesFrame.SaveClicked(OnlyShown: boolean);
var
  Dlg: TSaveDialog;
  s: String;
  Filename: String;
  fs: TFileStreamUTF8;
begin
  Dlg:=TSaveDialog.Create(nil);
  try
    Dlg.Title:=lisSaveMessages;
    Dlg.FileName:='messages.txt';
    Dlg.Options:=Dlg.Options+[ofPathMustExist,ofCreatePrompt];
    InitIDEFileDialog(Dlg);
    if not Dlg.Execute then exit;
    Filename:=TrimAndExpandFilename(Dlg.FileName);
    if DirPathExistsCached(Filename) then exit;

    s:=AllMessagesAsString(OnlyShown);

    try
      fs:=TFileStreamUTF8.Create(Filename,fmCreate);
      try
        if s<>'' then
          fs.Write(s[1],length(s));
      finally
        fs.Free;
      end;
    except
      on E: Exception do begin
        IDEMessageDialog(lisWriteError, Format(lisUnableToWriteFile2, [Filename]
          ),
          mtError, [mbCancel]);
      end;
    end;

  finally
    StoreIDEFileDialog(Dlg);
    Dlg.Free;
  end;
end;

procedure TMessagesFrame.CopyAllClicked(OnlyShown: boolean);
var
  s: String;
  Msg: String;
begin
  s:=AllMessagesAsString(OnlyShown);
  if length(s)>1000000 then begin
    if length(s)<10000 then
      Msg:=Format(lisByte, [IntToStr(length(s))])
    else if Length(s)<10000000 then
      Msg:=Format(lisKB, [IntToStr(length(s) div 1000)])
    else
      Msg:=Format(lisMB, [IntToStr(length(s) div 1000)]);
    if IDEMessageDialog(lisCCOWarningCaption, Format(
      lisThisWillPutALotOfTextOnTheClipboardProceed, [Msg, #13]),
      mtConfirmation,[mbYes,mbNo])<>mrYes then exit;
  end;
  Clipboard.AsText:=s;
end;

procedure TMessagesFrame.CopyMsgToClipboard(OnlyFilename: boolean);
var
  View: TLMsgWndView;
  LineNumber: Integer;
  Txt: String;
  Line: TMessageLine;
begin
  View:=MessagesCtrl.SelectedView;
  if View=nil then exit;
  LineNumber:=MessagesCtrl.SelectedLine;
  if LineNumber<0 then begin
    // header
    if OnlyFilename then exit;
    Txt:=MessagesCtrl.GetHeaderText(View);
  end else if LineNumber<View.Lines.Count then begin
    // normal messages
    Line:=View.Lines[LineNumber];
    if OnlyFilename then
      Txt:=Line.Filename
    else
      Txt:=MessagesCtrl.GetLineText(Line);
  end else if LineNumber=View.Lines.Count then begin
    // progress line
    Line:=View.ProgressLine;
    if OnlyFilename then
      Txt:=Line.Filename
    else
      Txt:=MessagesCtrl.GetLineText(Line);
  end else
    exit;
  Clipboard.AsText:=Txt;
end;

function TMessagesFrame.GetMsgPattern(SubTool: string; MsgId: integer;
  WithUrgency: boolean; MaxLen: integer): string;
var
  Pattern: String;
  Urgency: TMessageLineUrgency;
begin
  Result:=SubTool;
  if Result=SubToolFPC then
    Result:='';
  if (MsgID<>0) then
    Result+='('+IntToStr(MsgID)+')';
  Pattern:=ExternalToolList.GetMsgPattern(SubTool,MsgID,Urgency);
  if Pattern<>'' then
    Result+=' '+Pattern;
  if WithUrgency and (not (Urgency in [mluNone,mluImportant])) then
    Result:=MessagesCtrl.UrgencyToStr(Urgency)+': '+Result;
  if UTF8Length(Result)>MaxLen then
    Result:=UTF8Copy(Result,1,MaxLen)+'...';
end;

procedure TMessagesFrame.Notification(AComponent: TComponent;
  Operation: TOperation);
begin
  inherited Notification(AComponent, Operation);
  if Operation=opRemove then begin
    if AComponent=MessagesCtrl then
      MessagesCtrl:=nil;
  end;
end;

constructor TMessagesFrame.Create(TheOwner: TComponent);
var
  ImgIDInfo: Integer;
  ImgIDHint: Integer;
  ImgIDNote: Integer;
  ImgIDWarning: Integer;
  ImgIDError: Integer;
  ImgIDFatal: Integer;
begin
  inherited Create(TheOwner);

  MessagesCtrl:=TMessagesCtrl.Create(Self);
  ImgIDInfo:=IDEImages.LoadImage(12, 'state12x12_information');
  ImgIDHint:=IDEImages.LoadImage(12, 'state12x12_hint');
  ImgIDNote:=IDEImages.LoadImage(12, 'state12x12_note');
  ImgIDWarning:=IDEImages.LoadImage(12, 'state12x12_warning');
  ImgIDError:=IDEImages.LoadImage(12, 'state12x12_error');
  ImgIDFatal:=IDEImages.LoadImage(12, 'state12x12_fatal');
  with MessagesCtrl do begin
    Name:='MessagesCtrl';
    Align:=alClient;
    Parent:=Self;

    UrgencyStyles[mluNone].SetValues('?',ImgIDInfo,EnvironmentOptions.MsgColors[mluNone]);
    UrgencyStyles[mluProgress].SetValues(lisPDProgress, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluProgress]);
    UrgencyStyles[mluDebug].SetValues(lisDebug, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluDebug]);
    UrgencyStyles[mluVerbose3].SetValues(lisExtremelyVerbose, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluVerbose3]);
    UrgencyStyles[mluVerbose2].SetValues(lisVeryVerbose, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluVerbose2]);
    UrgencyStyles[mluVerbose].SetValues(lisVerbose, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluVerbose]);
    UrgencyStyles[mluHint].SetValues(lisHint, ImgIDHint,
      EnvironmentOptions.MsgColors[mluHint]);
    UrgencyStyles[mluNote].SetValues(lisNote, ImgIDNote,
      EnvironmentOptions.MsgColors[mluNote]);
    UrgencyStyles[mluWarning].SetValues(lisCCOWarningCaption, ImgIDWarning,
      EnvironmentOptions.MsgColors[mluWarning]);
    UrgencyStyles[mluImportant].SetValues(lisImportant, ImgIDInfo,
      EnvironmentOptions.MsgColors[mluImportant]);
    UrgencyStyles[mluError].SetValues(lisCCOErrorCaption, ImgIDError,
      EnvironmentOptions.MsgColors[mluError]);
    UrgencyStyles[mluFatal].SetValues(lisFatal, ImgIDFatal,
      EnvironmentOptions.MsgColors[mluFatal]);
    UrgencyStyles[mluPanic].SetValues(lisPanic, ImgIDFatal,
      EnvironmentOptions.MsgColors[mluPanic]);
    Images:=IDEImages.Images_12;
    PopupMenu:=MsgCtrlPopupMenu;
  end;
  MessagesCtrl.SourceMarks:=ExtToolsMarks;

  // search
  SearchPanel.Visible:=false; // by default the search is hidden
  HideSearchSpeedButton.Hint:=lisHideSearch;
  HideSearchSpeedButton.LoadGlyphFromResourceName(HInstance, 'debugger_power_grey');
  SearchNextSpeedButton.Hint:=lisUDSearchNextOccurrenceOfThisPhrase;
  SearchNextSpeedButton.LoadGlyphFromResourceName(HInstance, 'callstack_bottom');
  SearchPrevSpeedButton.Hint:=lisUDSearchPreviousOccurrenceOfThisPhrase;
  SearchPrevSpeedButton.LoadGlyphFromResourceName(HInstance, 'callstack_top');
  SearchEdit.TextHint:=lisUDSearch;
end;

destructor TMessagesFrame.Destroy;
begin
  MessagesCtrl.BeginUpdate;
  ClearViews(false);
  inherited Destroy;
end;

procedure TMessagesFrame.ApplyIDEOptions;
begin
  MessagesCtrl.ApplyEnvironmentOptions;
end;

function TMessagesFrame.ViewCount: integer;
begin
  Result:=MessagesCtrl.ViewCount;
end;

function TMessagesFrame.GetView(aCaption: string; CreateIfNotExist: boolean
  ): TLMsgWndView;
begin
  Result:=MessagesCtrl.GetView(aCaption,CreateIfNotExist);
end;

function TMessagesFrame.FindUnfinishedView: TLMsgWndView;
begin
  Result:=MessagesCtrl.FindUnfinishedView;
end;

procedure TMessagesFrame.DeleteView(View: TLMsgWndView);
begin
  View.Free;
end;

function TMessagesFrame.IndexOfView(View: TLMsgWndView): integer;
begin
  Result:=MessagesCtrl.IndexOfView(View);
end;

procedure TMessagesFrame.ClearViews(OnlyFinished: boolean);
begin
  MessagesCtrl.ClearViews(OnlyFinished);
end;

procedure TMessagesFrame.CreateMarksForFile(aSynEdit: TSynEdit;
  aFilename: string; DeleteOld: boolean);
begin
  MessagesCtrl.CreateMarksForFile(aSynEdit,aFilename,DeleteOld);
end;

procedure TMessagesFrame.ApplySrcChanges(Changes: TETSingleSrcChanges);
begin
  MessagesCtrl.ApplySrcChanges(Changes);
end;

procedure TMessagesFrame.ApplyMultiSrcChanges(Changes: TETMultiSrcChanges);
var
  Node: TAvgLvlTreeNode;
begin
  for Node in Changes.PendingChanges do
    ApplySrcChanges(TETSingleSrcChanges(Node.Data));
end;

procedure TMessagesFrame.SourceEditorPopup(MarkLine: TSynEditMarkLine;
  const LogicalCaretXY: TPoint);
var
  i: Integer;
  CurMark: TETMark;
  BestMark: TETMark;
begin
  //debugln(['TMessagesFrame.SourceEditorPopup ']);
  // show quickfixes for the first TETMark in editor line
  if MarkLine=nil then exit;
  IDEQuickFixes.ClearLines;
  BestMark:=nil;
  for i:=0 to MarkLine.Count-1 do begin
    CurMark:=TETMark(MarkLine[i]);
    if not (CurMark is TETMark) then continue;
    //debugln(['TMessagesFrame.SourceEditorPopup ',CurMark.Line,',',CurMark.Column,' ID=',CurMark.MsgLine.MsgID,' Msg=',CurMark.MsgLine.Msg,' EditorXY=',dbgs(LogicalCaretXY)]);
    if (BestMark=nil) then
      BestMark:=CurMark
    else begin
      // there are multiple marks in the line
      if (LogicalCaretXY.Y=MarkLine.LineNum)
      and (LogicalCaretXY.X=CurMark.Column) then begin
        // mark at cursor position
        BestMark:=CurMark;
        break;
      end else begin
        // default: use first in line
        if CurMark.Column<BestMark.Column then
          BestMark:=CurMark;
      end;
    end;
  end;
  if BestMark=nil then
    exit;
  IDEQuickFixes.AddMsgLine(BestMark.MsgLine);
  // create items
  if IDEQuickFixes.Count>0 then begin
    IDEQuickFixes.OnPopupMenu(SrcEditMenuSectionFirstDynamic);
    if mcoSrcEditPopupSelect in MessagesCtrl.Options then
      MessagesCtrl.Select(BestMark.MsgLine,true);
  end;
end;

procedure TMessagesFrame.SourceEditorHint(MarkLine: TSynEditMarkLine;
  var HintStr: string);
var
  i: Integer;
  CurMark: TETMark;
  Msg: TMessageLine;
  CurHint: String;
begin
  if MarkLine=nil then exit;
  for i:=0 to MarkLine.Count-1 do begin
    CurMark:=TETMark(MarkLine[i]);
    if not (CurMark is TETMark) then continue;
    Msg:=CurMark.MsgLine;
    CurHint:=MessagesCtrl.UrgencyToStr(Msg.Urgency)+': '+Msg.Msg;
    if HintStr<>'' then HintStr:=HintStr+LineEnding;
    HintStr:=HintStr+CurHint;
  end;
end;

procedure TMessagesFrame.SelectMsgLine(Msg: TMessageLine; DoScroll: boolean);
begin
  MessagesCtrl.Select(Msg,DoScroll);
end;

function TMessagesFrame.SelectFirstUrgentMessage(
  aMinUrgency: TMessageLineUrgency; WithSrcPos: boolean): boolean;
begin
  Result:=MessagesCtrl.SelectFirstUrgentMessage(aMinUrgency,WithSrcPos);
end;

function TMessagesFrame.SelectNextUrgentMessage(
  aMinUrgency: TMessageLineUrgency; WithSrcPos, Downwards: boolean): boolean;
begin
  Result:=MessagesCtrl.SelectNextUrgentMessage(aMinUrgency,WithSrcPos,Downwards);
end;

procedure TMessagesFrame.ClearCustomMessages(const ViewCaption: string);
var
  View: TLMsgWndView;
begin
  View:=GetView(ViewCaption,false);
  if (View=nil) or (View.Lines.Count=0) then exit;
  View.Lines.Clear;
  MessagesCtrl.UpdateScrollBar(true);
  MessagesCtrl.Invalidate;
end;

function TMessagesFrame.AddCustomMessage(TheUrgency: TMessageLineUrgency;
  Msg: string; aFilename: string; LineNumber: integer; Column: integer;
  const ViewCaption: string): TMessageLine;
var
  View: TLMsgWndView;
begin
  Result:=nil;
  View:=GetView(ViewCaption,true);
  View.Running:=false;
  Result:=View.Lines.CreateLine(-1);
  Result.Msg:=Msg;
  Result.Urgency:=TheUrgency;
  Result.SetSourcePosition(aFilename,LineNumber,Column);
  View.Lines.Add(Result);
  MessagesCtrl.UpdateScrollBar(true);
  MessagesCtrl.Invalidate;
end;

end.

